Django sessions and cookies
last modified November 13, 2026 Python 3.15
HTTP is stateless, so a web application has to recognize the returning visitor in some other way. In this article we use the
two tools that Django offers for this: sessions, which keep the data on the server, and cookies, which keep it in the browser. We
configure the session cookie, count visits and fill a shopping cart with request.session, look at how the data
is stored, compare the session backends, set plain and signed cookies, and test the code. The examples use Django 6.1 and
Python 3.15.
How sessions work
A session is a dictionary that belongs to one browser. The data stays on the server, in the database by default; the
browser gets only a random key in a cookie, which is sent back with every request. The middleware
SessionMiddleware reads the cookie at the beginning of a request and loads the data lazily, the first time the code uses
request.session; at the end of the request it saves the dictionary if it was modified and sets the cookie. A response
that depended on the session carries the header Vary: Cookie, so that caches do not mix up the visitors. The values
of a session are serialized to JSON, so they may be strings, numbers, booleans, lists, dictionaries, and None.
A cookie is a small piece of text that the server sends in the header Set-Cookie and the browser
returns in the header Cookie. Everything in a cookie belongs to the user, who can read it, delete it, or change it; so a cookie
is good for harmless preferences and for the session key, and the data that matters is kept in the session.
The example project
The project is created with uv; the steps are explained in
Django introduction. The commands below create a project
directory, install Django, generate the project with the configuration package
config, start the app shop, and create the folder demos for demo scripts.
$ mkdir sessions $ cd sessions $ uv init --bare $ uv add django $ uv run django-admin startproject config . $ uv run python manage.py startapp shop $ mkdir demos
The trailing dot in startproject config . creates manage.py and the
config package in the current directory instead of one level deeper. A new app is
ignored by Django until it is listed in INSTALLED_APPS, so we add
the app to the list in config/settings.py; the other
entries are generated by startproject.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'shop',
]
The listings in this article are of two kinds. A listing with a solid blue border and the label
Project file is a file of the application: models, views, templates, tests, and
settings. A listing with a dashed amber border and the label Demo script is a small
helper in the demos folder; it sets up Django, runs a few lines of code, and prints
the results, so that we can watch the behaviour. Demo scripts are not part of the application;
we run one from the project directory with uv run python -m demos.name.
The app shop has a few small views that use the session and cookies. The table of the sessions is created by
migrate, since django.contrib.sessions is a part of INSTALLED_APPS, and the middleware
is in MIDDLEWARE of the generated settings.
$ uv run python manage.py migrate | tail -n 2 Applying auth.0012_alter_user_first_name_max_length... OK Applying sessions.0001_initial... OK
Session settings
The session cookie is configured by the settings with the prefix SESSION_. We rename the cookie, so that it does
not clash with the cookies of other projects on localhost, shorten its life to one week, and spell out the
security options. SESSION_COOKIE_HTTPONLY hides the cookie from JavaScript, so that a script injected into a page cannot steal the
session. SESSION_COOKIE_SAMESITE controls whether the browser sends the cookie with requests that come from other sites;
Lax sends it with top-level navigation, but not with embedded requests and forms posted from another site.
SESSION_COOKIE_SECURE restricts the cookie to HTTPS; it is off here, because the development server speaks plain HTTP,
and must be on in production.
SESSION_COOKIE_NAME = 'shopsid' SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 SESSION_COOKIE_SAMESITE = 'Lax' SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SECURE = False SESSION_SAVE_EVERY_REQUEST = False SESSION_EXPIRE_AT_BROWSER_CLOSE = False
The script compares the project values with the defaults from django.conf.global_settings. The option
SESSION_SAVE_EVERY_REQUEST would save the session and renew its expiry on every request, not only when it
changes; SESSION_EXPIRE_AT_BROWSER_CLOSE makes a cookie that the browser deletes when it is closed.
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from django.conf import global_settings, settings
names = ['SESSION_ENGINE', 'SESSION_COOKIE_NAME', 'SESSION_COOKIE_AGE', 'SESSION_COOKIE_SAMESITE',
'SESSION_COOKIE_HTTPONLY', 'SESSION_COOKIE_SECURE', 'SESSION_COOKIE_DOMAIN',
'SESSION_COOKIE_PATH', 'SESSION_SAVE_EVERY_REQUEST', 'SESSION_EXPIRE_AT_BROWSER_CLOSE',
'SESSION_SERIALIZER']
print(f'{"setting":34} {"default":42} project')
for name in names:
default, value = getattr(global_settings, name), getattr(settings, name)
mark = '' if default == value else ' <-- changed'
print(f'{name:34} {default!s:42} {value}{mark}')
Only the name and the age differ from the defaults. The serializer is the JSON one; a pickle serializer existed in older versions, but it is dangerous and was removed in Django 5.0.
$ uv run python -m demos.settings setting default project SESSION_ENGINE django.contrib.sessions.backends.db django.contrib.sessions.backends.db SESSION_COOKIE_NAME sessionid shopsid <-- changed SESSION_COOKIE_AGE 1209600 604800 <-- changed SESSION_COOKIE_SAMESITE Lax Lax SESSION_COOKIE_HTTPONLY True True SESSION_COOKIE_SECURE False False SESSION_COOKIE_DOMAIN None None SESSION_COOKIE_PATH / / SESSION_SAVE_EVERY_REQUEST False False SESSION_EXPIRE_AT_BROWSER_CLOSE False False SESSION_SERIALIZER django.contrib.sessions.serializers.JSONSerializer django.contrib.sessions.serializers.JSONSerializer
Using request.session
The object request.session behaves like a dictionary: it supports reading with [] and
get(), assignment, del, pop(), and in. The view visits counts the
requests of a visitor. The view cart_add keeps a dictionary of the items in the session: it reads the cart, changes it, and assigns it
back. The assignment is important, as we shall see later. The method flush() in the view reset removes the data and the session row,
and tells the browser to delete the cookie; a logout does the same.
from django.core.signing import BadSignature
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.views.decorators.http import require_POST
THEMES = {'light', 'dark'}
def text(message, **kwargs):
return HttpResponse(message + '\n', content_type='text/plain', **kwargs)
def visits(request):
count = request.session.get('visits', 0) + 1
request.session['visits'] = count
return text(f'Visit number {count}')
def set_theme(request, name):
if name not in THEMES:
return text('Unknown theme', status=400)
response = text(f'Theme set to {name}')
response.set_cookie('theme', name, max_age=60 * 60 * 24 * 365, samesite='Lax')
return response
def show_theme(request):
theme = request.COOKIES.get('theme')
if theme not in THEMES:
theme = 'light'
return text(f'Theme: {theme}')
def remember(request, name):
response = text(f'I will remember {name}')
response.set_signed_cookie('remember', name, salt='remember', max_age=3600,
httponly=True, samesite='Lax')
return response
def whoami(request):
try:
name = request.get_signed_cookie('remember', salt='remember', max_age=3600)
except KeyError:
return text('Nobody here')
except BadSignature:
return text('The cookie was changed or has expired', status=400)
return text(f'Hello, {name}')
@require_POST
def cart_add(request, sku):
cart = request.session.get('cart', {})
cart[sku] = cart.get(sku, 0) + 1
request.session['cart'] = cart
return JsonResponse(cart)
def cart_show(request):
return JsonResponse(request.session.get('cart', {}))
@require_POST
def reset(request):
request.session.flush()
return text('Session deleted')
The same file contains the views for the cookies, which we discuss below. The views that change state, cart_add and
reset, accept only POST. The URL patterns follow.
from django.urls import path
from . import views
urlpatterns = [
path('visits/', views.visits),
path('theme/', views.show_theme),
path('theme/<slug:name>/', views.set_theme),
path('remember/<slug:name>/', views.remember),
path('whoami/', views.whoami),
path('cart/', views.cart_show),
path('cart/add/<slug:sku>/', views.cart_add),
path('reset/', views.reset),
]
The root URL configuration includes the patterns of the app at the top level.
from django.urls import include, path
urlpatterns = [
path('', include('shop.urls')),
]
The session middleware and the cookies are tied to the HTTP exchange, so the best way to see them is the real server. We
start the development server in one terminal and use curl in another.
$ uv run python manage.py runserver
The option -i of curl prints the response headers, and -c stores the received cookies in a file that
the option -b sends back. The first response has the header Set-Cookie with the random key
and the attributes from our settings: the expiry date, HttpOnly, Max-Age of one week, Path=/, and
SameSite=Lax. With the cookie jar the next requests belong to the same session, so the counter increases. The jar file
shows what the client keeps; the prefix #HttpOnly_ is how curl marks the cookie. A request without the cookie starts a new session.
$ curl -si -c jar.txt http://127.0.0.1:8000/visits/ HTTP/1.1 200 OK Date: Fri, 13 Nov 2026 20:33:05 GMT Server: WSGIServer CPython/3.15.2 Content-Type: text/plain X-Frame-Options: DENY Content-Length: 15 Vary: Cookie X-Content-Type-Options: nosniff Referrer-Policy: same-origin Cross-Origin-Opener-Policy: same-origin Set-Cookie: shopsid=78f31pzlmiwuinqyg039wnzw6kxzph82; expires=Fri, 20 Nov 2026 20:33:05 GMT; HttpOnly; Max-Age=604800; Path=/; SameSite=Lax Visit number 1 $ curl -s -b jar.txt -c jar.txt http://127.0.0.1:8000/visits/ Visit number 2 $ curl -s -b jar.txt -c jar.txt http://127.0.0.1:8000/visits/ Visit number 3 $ cat jar.txt # Netscape HTTP Cookie File # https://curl.se/docs/http-cookies.html # This file was generated by libcurl! Edit at your own risk. #HttpOnly_127.0.0.1 FALSE / FALSE 1795206785 shopsid 78f31pzlmiwuinqyg039wnzw6kxzph82 $ curl -s http://127.0.0.1:8000/visits/ Visit number 1
Shopping cart in the session
The script uses the test client, which keeps cookies like a browser, to simulate two visitors. The first adds items to
the cart; the second only looks. Each client gets its own session, and the data is separated. The client stores the cookie named
by our setting. The GET request to a view that accepts only POST fails with 405. The POST to reset deletes the session.
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from django.test import Client
from django.test.utils import setup_test_environment
setup_test_environment()
import logging
from django.contrib.sessions.models import Session
logging.getLogger('django.request').setLevel(logging.ERROR)
Session.objects.all().delete()
alice, bob = Client(), Client()
print(alice.post('/cart/add/book/').json())
print(alice.post('/cart/add/book/').json())
print(alice.post('/cart/add/pen/').json())
print('alice cart:', alice.get('/cart/').json())
print('bob cart: ', bob.get('/cart/').json())
print('cookies of alice:', list(alice.cookies))
print('cookies of bob: ', list(bob.cookies))
print('sessions in the database:', Session.objects.count())
print()
print('GET /cart/add/book/:', alice.get('/cart/add/book/').status_code)
print('POST /reset/:', alice.post('/reset/').content.decode().strip())
print('alice cart after reset:', alice.get('/cart/').json())
print('sessions in the database:', Session.objects.count())
print('cookie after reset:', alice.cookies['shopsid'].value or '(empty)',
'| max-age:', alice.cookies['shopsid']['max-age'])
The cart of the first visitor holds the two books and the pen across the requests, and the other visitor's cart is empty. There is
one row in the database table, since the second client never changed its session, and Django does not create a row for an untouched
one. After flush() the row is gone, and the response replaces the cookie with an empty one that expires at once,
Max-Age=0.
$ uv run python -m demos.flow
{'book': 1}
{'book': 2}
{'book': 2, 'pen': 1}
alice cart: {'book': 2, 'pen': 1}
bob cart: {}
cookies of alice: ['shopsid']
cookies of bob: []
sessions in the database: 1
GET /cart/add/book/: 405
POST /reset/: Session deleted
alice cart after reset: {}
sessions in the database: 0
cookie after reset: (empty) | max-age: 0
Inside the session store
The class SessionStore of the backend can be used without a request, which shows what the middleware does. The
rows are the instances of the model Session with the fields session_key, session_data, and expire_date.
The script saves a session and looks at its row, plays with the expiry, shows the effect of changing a mutable value, rotates the key,
stores a value that JSON cannot represent, and removes the expired rows.
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from datetime import datetime
from django.contrib.sessions.backends.db import SessionStore
from django.contrib.sessions.models import Session
from django.core.management import call_command
from django.utils import timezone
Session.objects.all().delete()
store = SessionStore()
store['visits'] = 1
store['cart'] = {'book': 2}
print('key before save:', store.session_key)
store.save()
print('key after save:', len(store.session_key), 'characters')
print('rows in django_session:', Session.objects.count())
row = Session.objects.get(pk=store.session_key)
print('session_data:', row.session_data[:48] + '...')
print('decoded:', store.decode(row.session_data))
print('expires in:', round((row.expire_date - timezone.now()).total_seconds()), 'seconds')
print('get_expiry_age():', store.get_expiry_age())
print()
store.set_expiry(300)
print('set_expiry(300):', store.get_expiry_age(), '| at browser close:', store.get_expire_at_browser_close())
store.set_expiry(0)
print('set_expiry(0):', store.get_expire_at_browser_close())
store.set_expiry(None)
print('set_expiry(None):', store.get_expiry_age())
print()
store.save()
store = SessionStore(store.session_key)
print('modified after loading:', store.modified)
store['cart']['pen'] = 1
print('modified after a change inside the dict:', store.modified)
store['cart'] = store['cart']
print('modified after assigning the key again:', store.modified)
print()
old_key = store.session_key
store.cycle_key()
print('cycle_key(): key changed:', store.session_key != old_key, '| data kept:', dict(store))
print('rows:', Session.objects.count())
try:
store['when'] = datetime(2026, 11, 13)
store.save()
except TypeError as error:
print('datetime value:', error)
del store['when']
print()
Session.objects.update(expire_date=timezone.now() - timezone.timedelta(days=1))
print('expired rows before clearsessions:', Session.objects.count())
call_command('clearsessions')
print('rows after clearsessions:', Session.objects.count())
store = SessionStore()
store['x'] = 1
store.save()
print('rows:', Session.objects.count())
store.flush()
print('rows after flush():', Session.objects.count(), '| key:', store.session_key)
The key is created at the first save; it has 32 characters, the alphabet of lower case letters and digits. The column
session_data holds the base64-encoded JSON, followed by a timestamp and a signature, so that a modified row is detected
when it is loaded. set_expiry() accepts a number of seconds, 0 for a cookie that lasts until the browser is closed,
a datetime, or None to return to SESSION_COOKIE_AGE. The next part is the well-known trap: a change
made inside a value, such as store['cart']['pen'] = 1, does not set the flag modified, so the middleware would
not save it. We assign the key again, or set modified = True, which is why the cart view assigns the cart back. The method
cycle_key() keeps the data and changes the key; Django calls it on login, to prevent session fixation, an attack in which the
attacker lets the victim log in with a session key that the attacker knows. A value like a datetime must be converted to a string or a number first. Expired rows
are not deleted automatically; the command clearsessions removes them, and a production site runs it from cron once a
day. The last lines show that flush() deletes the row and forgets the key.
$ uv run python -m demos.store
key before save: None
key after save: 32 characters
rows in django_session: 1
session_data: eyJ2aXNpdHMiOjEsImNhcnQiOnsiYm9vayI6Mn19:1xRxxK:...
decoded: {'visits': 1, 'cart': {'book': 2}}
expires in: 604800 seconds
get_expiry_age(): 604800
set_expiry(300): 300 | at browser close: False
set_expiry(0): True
set_expiry(None): 604800
modified after loading: False
modified after a change inside the dict: False
modified after assigning the key again: True
cycle_key(): key changed: True | data kept: {'visits': 1, 'cart': {'book': 2, 'pen': 1}}
rows: 1
datetime value: Object of type datetime is not JSON serializable
expired rows before clearsessions: 1
rows after clearsessions: 0
rows: 1
rows after flush(): 0 | key: None
Session backends
The setting SESSION_ENGINE selects where the data lives. The database backend, the default, is reliable and needs
no setup. The cache backend keeps the data only in the cache, which is fast, but the sessions disappear when the cache is cleared or
restarted. The backend cached_db writes to both and reads from the cache first. The file backend keeps a file per
session in SESSION_FILE_PATH. The backend signed_cookies puts the whole data into the cookie, signed with
the SECRET_KEY, and the server stores nothing. The script saves the same data with each backend and loads it back.
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
import tempfile
from importlib import import_module
from django.test.utils import override_settings
engines = [
'django.contrib.sessions.backends.db',
'django.contrib.sessions.backends.cache',
'django.contrib.sessions.backends.cached_db',
'django.contrib.sessions.backends.file',
'django.contrib.sessions.backends.signed_cookies',
]
with tempfile.TemporaryDirectory() as folder, override_settings(SESSION_FILE_PATH=folder):
for engine in engines:
store = import_module(engine).SessionStore()
store['user'] = 'alice'
store['cart'] = {'book': 2, 'pen': 1}
store.save()
key = store.session_key
print(f'{engine.rsplit(".", 1)[1]:15} key of {len(key):3} characters: {key[:36]}')
reloaded = import_module(engine).SessionStore(key)
print(f'{"":15} loaded again: {dict(reloaded)}')
For the first four backends the key is a random identifier. For signed_cookies the key is the data itself, so it has
107 characters here and grows with the data; the browsers limit a cookie to about 4 KB. The signature protects the content from changes, but
it is not encrypted, so the user can read it and must not receive secrets. Such a session cannot be invalidated on the server, and an old cookie
stays valid until it expires. For these reasons the database or cached_db backends are the safer choice.
$ uv run python -m demos.engines
db key of 32 characters: fh65fnzgtw2yqd48g7d1zqzib9p94m44
loaded again: {'user': 'alice', 'cart': {'book': 2, 'pen': 1}}
cache key of 32 characters: qnea6hn9xg5yaexqpf5v9u1943lyk4ox
loaded again: {'user': 'alice', 'cart': {'book': 2, 'pen': 1}}
cached_db key of 32 characters: xkpmf7wjkgdjhm7dd0uvryb3qw9efur7
loaded again: {'user': 'alice', 'cart': {'book': 2, 'pen': 1}}
file key of 32 characters: yyf047zs58v3308yg0qghdhaegz0s7y7
loaded again: {'user': 'alice', 'cart': {'book': 2, 'pen': 1}}
signed_cookies key of 107 characters: eyJ1c2VyIjoiYWxpY2UiLCJjYXJ0Ijp7ImJv
loaded again: {'user': 'alice', 'cart': {'book': 2, 'pen': 1}}
Cookies
Not every piece of data needs a session. A preference such as the theme of a page can be stored in a cookie, without a row in the database. The method
response.set_cookie() takes the name, the value, and the options: max_age is the lifetime in seconds, secure sends the cookie only
over HTTPS, httponly hides it from scripts, samesite limits cross-site sending, and domain and path select
the URLs to which it is sent. Without max_age the cookie is deleted when the browser closes. The cookies of a request are in the dictionary request.COOKIES, and
the value is always a string from the user, so the view show_theme checks it against the set of known themes.
The signed cookies solve a different problem: the user can read the value, but cannot change it. The method
set_signed_cookie() appends a timestamp and a signature, computed with the SECRET_KEY and an optional salt, which makes
the signatures of different purposes incompatible. The method get_signed_cookie() checks the signature and the age given in
max_age; it raises KeyError when the cookie is missing and BadSignature (also
SignatureExpired) when the check fails, or returns the default if one is given. The script reads and sets the cookies, and
then changes them as a user could. It lowers the log level of django.request, to keep the output clean.
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from django.test import Client
from django.test.utils import setup_test_environment
setup_test_environment()
import logging
logging.getLogger('django.request').setLevel(logging.ERROR)
client = Client()
def show(label, response):
print(f'{label}: {response.status_code} {response.content.decode().strip()!r}')
for name, morsel in response.cookies.items():
attributes = {key: value for key, value in morsel.items() if value}
print(f' {name} = {morsel.value[:28]}{"..." if len(morsel.value) > 28 else ""}')
print(f' {attributes}')
show('visit', client.get('/visits/'))
show('visit again', client.get('/visits/'))
show('theme', client.get('/theme/dark/'))
show('theme read', client.get('/theme/'))
show('unknown theme', client.get('/theme/blue/'))
client.cookies['theme'] = '<script>'
show('hostile cookie', client.get('/theme/'))
print()
show('remember', client.get('/remember/alice/'))
show('whoami', client.get('/whoami/'))
value = client.cookies['remember'].value
print('signed value:', value)
client.cookies['remember'] = value.replace('alice', 'admin', 1)
show('changed value', client.get('/whoami/'))
client.cookies['remember'] = value.rsplit(':', 1)[0] + ':' + 'x' * 27
show('wrong signature', client.get('/whoami/'))
The session cookie and the theme cookie carry the attributes that the settings and the arguments gave them; the session
cookie has httponly, the theme cookie has not. The hostile value sent in the theme cookie is ignored, and the unknown theme in
the URL is answered with 400. The signed value has three parts separated by colons: the value, the timestamp in base 62, and the
signature. Changing the name to admin or replacing the signature makes get_signed_cookie() raise
BadSignature, and the view returns 400. A signed cookie is still not a safe place for secrets, but it can safely carry
an identifier that the server later trusts.
$ uv run python -m demos.cookies
visit: 200 'Visit number 1'
shopsid = eoaqepa1gv1le6u13aaw7ajjqm05...
{'expires': 'Fri, 20 Nov 2026 20:33:07 GMT', 'path': '/', 'max-age': 604800, 'httponly': True, 'samesite': 'Lax'}
visit again: 200 'Visit number 2'
shopsid = eoaqepa1gv1le6u13aaw7ajjqm05...
{'expires': 'Fri, 20 Nov 2026 20:33:07 GMT', 'path': '/', 'max-age': 604800, 'httponly': True, 'samesite': 'Lax'}
theme: 200 'Theme set to dark'
theme = dark
{'expires': 'Sat, 13 Nov 2027 20:33:07 GMT', 'path': '/', 'max-age': 31536000, 'samesite': 'Lax'}
theme read: 200 'Theme: dark'
unknown theme: 400 'Unknown theme'
hostile cookie: 200 'Theme: light'
remember: 200 'I will remember alice'
remember = alice:1xRxxL:vAmiU69iwtWoVDj...
{'expires': 'Fri, 13 Nov 2026 21:33:07 GMT', 'path': '/', 'max-age': 3600, 'httponly': True, 'samesite': 'Lax'}
whoami: 200 'Hello, alice'
signed value: alice:1xRxxL:vAmiU69iwtWoVDjbfikncsW1lOR5ACMjmsVEW3KnQzM
changed value: 400 'The cookie was changed or has expired'
wrong signature: 400 'The cookie was changed or has expired'
Testing sessions
The test client stores cookies between requests, so the tests can follow a visitor. The property client.session returns the session
of the client, or creates it; after we change it we must call save(), and the next request carries the data. The decorator
override_settings switches the backend for a test. The tests check the counter, the separation of the visitors, the
cart, the method restriction, the deletion by flush(), the cookie attributes, both kinds of cookies, and the signed-cookie backend.
from django.test import Client, TestCase, override_settings
class SessionTests(TestCase):
def test_counter_lives_in_the_session(self):
for expected in (1, 2, 3):
self.assertContains(self.client.get('/visits/'), f'Visit number {expected}')
def test_sessions_are_separate(self):
self.client.post('/cart/add/book/')
other = Client()
self.assertEqual(other.get('/cart/').json(), {})
self.assertEqual(self.client.get('/cart/').json(), {'book': 1})
def test_cart_accumulates(self):
self.client.post('/cart/add/book/')
response = self.client.post('/cart/add/book/')
self.assertEqual(response.json(), {'book': 2})
def test_cart_add_needs_post(self):
self.assertEqual(self.client.get('/cart/add/book/').status_code, 405)
def test_reset_deletes_the_session(self):
self.client.post('/cart/add/book/')
self.client.post('/reset/')
self.assertEqual(self.client.get('/cart/').json(), {})
def test_session_cookie_attributes(self):
response = self.client.get('/visits/')
morsel = response.cookies['shopsid']
self.assertTrue(morsel['httponly'])
self.assertEqual(morsel['samesite'], 'Lax')
self.assertEqual(morsel['max-age'], 60 * 60 * 24 * 7)
def test_session_can_be_prepared_in_a_test(self):
session = self.client.session
session['visits'] = 41
session.save()
self.assertContains(self.client.get('/visits/'), 'Visit number 42')
@override_settings(SESSION_ENGINE='django.contrib.sessions.backends.signed_cookies')
def test_signed_cookie_engine_keeps_data_in_the_cookie(self):
self.client.post('/cart/add/book/')
self.assertEqual(self.client.get('/cart/').json(), {'book': 1})
self.assertGreater(len(self.client.cookies['shopsid'].value), 40)
class CookieTests(TestCase):
def test_theme_cookie(self):
response = self.client.get('/theme/dark/')
self.assertEqual(response.cookies['theme'].value, 'dark')
self.assertContains(self.client.get('/theme/'), 'Theme: dark')
def test_unknown_theme_is_rejected(self):
self.assertEqual(self.client.get('/theme/blue/').status_code, 400)
def test_cookie_value_is_not_trusted(self):
self.client.cookies['theme'] = 'anything'
self.assertContains(self.client.get('/theme/'), 'Theme: light')
def test_signed_cookie_round_trip(self):
self.client.get('/remember/alice/')
self.assertContains(self.client.get('/whoami/'), 'Hello, alice')
def test_tampered_signed_cookie_is_refused(self):
self.client.get('/remember/alice/')
value = self.client.cookies['remember'].value
self.client.cookies['remember'] = value.replace('alice', 'admin', 1)
self.assertEqual(self.client.get('/whoami/').status_code, 400)
def test_no_cookie(self):
self.assertContains(self.client.get('/whoami/'), 'Nobody here')
All fourteen tests pass. The attributes are read from the morsel of the response cookie, where they have the lower-case keys that the standard library uses.
$ uv run python manage.py test Found 14 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). .............. ---------------------------------------------------------------------- Ran 14 tests in 0.020s OK Destroying test database for alias 'default'...
Source
Django documentation: How to use sessions
Django documentation: Session settings
Django documentation: Cryptographic signing
In this article we have configured the session cookie, kept a counter and a cart in request.session, looked at the
session store and its backends, set plain and signed cookies, and tested the code. Related articles are
Django authentication, Django HttpResponse, and
Django HttpRequest.
Author
List all Django tutorials.