E-commerce Website (Basic)
Abstract
Section titled “Abstract”Every online store boils down to three moves: show products, let people add them to a cart, and total it up. This project builds exactly that in Flask — a routes-and-templates web app with a product list, an add-to-cart action, and a cart page that sums the prices. It’s the perfect introduction to server-side web development: routing, URL parameters, template rendering, and redirects. Then you’ll fix the one bug that makes the starter version unusable as a real site — a global cart shared by every visitor — and add quantities, checkout, and a database.
You will leave understanding:
- How Flask maps URLs to functions with
@app.route. - The request → process → redirect pattern (POST/Redirect/GET).
- How
render_templatepasses data into Jinja HTML. - Why a module-level
cartlist is a serious bug, and how sessions fix it.
Prerequisites
Section titled “Prerequisites”- Python 3.6 or above.
- A text editor or IDE.
- Flask:
pip install flask. - Basic HTML.
- Comfort with functions and lists/dicts.
Getting Started
Section titled “Getting Started”Create the project
Section titled “Create the project”- Create a folder named
ecommerce. - Inside it, create
ecommerce_website.pyand atemplates/folder. - Install Flask:
pip install flask.
Templates
Section titled “Templates”Flask looks for HTML in templates/. You’ll need index.html and cart.html:
<h1>Products</h1>
<ul>
{% for p in products %}
<li>{{ p.name }} — ${{ p.price }}
<a href="{{ url_for('add_to_cart', product_id=p.id) }}">Add to cart</a></li>
{% endfor %}
</ul>
<a href="{{ url_for('view_cart') }}">View cart</a><h1>Your Cart</h1>
<ul>{% for item in cart %}<li>{{ item.name }} — ${{ item.price }}</li>{% endfor %}</ul>
<p>Total: ${{ total_price }}</p>
<a href="{{ url_for('index') }}">Keep shopping</a>Write the code
Section titled “Write the code”ecommerce_website.py
pch.viewSource"""
E-commerce Website (Basic)
A Python application that simulates a basic e-commerce website.
Features include:
- Displaying a list of products.
- Adding products to a shopping cart.
- Calculating the total price.
"""
import os
import sys
from flask import Flask, render_template, request, redirect, url_for
app = Flask(__name__)
# Sample product data
products = [
{"id": 1, "name": "Laptop", "price": 800},
{"id": 2, "name": "Smartphone", "price": 500},
{"id": 3, "name": "Headphones", "price": 100},
{"id": 4, "name": "Keyboard", "price": 50},
]
# Shopping cart
cart = []
@app.route('/')
def index():
"""Display the list of products."""
return render_template('index.html', products=products)
@app.route('/add_to_cart/<int:product_id>')
def add_to_cart(product_id):
"""Add a product to the shopping cart."""
product = next((p for p in products if p['id'] == product_id), None)
if product:
cart.append(product)
return redirect(url_for('view_cart'))
@app.route('/cart')
def view_cart():
"""Display the shopping cart and total price."""
total_price = sum(item['price'] for item in cart)
return render_template('cart.html', cart=cart, total_price=total_price)
# Flask looks for templates on disk, so a self-contained single-file project
# has to put them there before the first request. Keeping them as strings
# means the project stays one file; writing them at startup means Jinja can
# actually find them.
TEMPLATES = {
"base.html": """<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>{% block title %}Shop{% endblock %}</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 40rem; margin: 2rem auto; }
table { border-collapse: collapse; width: 100%; }
td, th { text-align: left; padding: 0.4rem 0.6rem; border-bottom: 1px solid #ddd; }
a { color: #06c; }
</style></head>
<body>
<h1><a href="{{ url_for('index') }}">Shop</a></h1>
{% block body %}{% endblock %}
</body></html>""",
"index.html": """{% extends "base.html" %}
{% block body %}
<p><a href="{{ url_for('view_cart') }}">View cart</a></p>
<table>
<tr><th>Product</th><th>Price</th><th></th></tr>
{% for product in products %}
<tr>
<td>{{ product.name }}</td>
<td>${{ product.price }}</td>
<td><a href="{{ url_for('add_to_cart', product_id=product.id) }}">Add to cart</a></td>
</tr>
{% endfor %}
</table>
{% endblock %}""",
"cart.html": """{% extends "base.html" %}
{% block title %}Cart{% endblock %}
{% block body %}
{% if cart %}
<table>
<tr><th>Product</th><th>Price</th></tr>
{% for item in cart %}
<tr><td>{{ item.name }}</td><td>${{ item.price }}</td></tr>
{% endfor %}
<tr><th>Total</th><th>${{ total_price }}</th></tr>
</table>
{% else %}
<p>The cart is empty.</p>
{% endif %}
<p><a href="{{ url_for('index') }}">Keep shopping</a></p>
{% endblock %}""",
}
def write_templates():
"""Put the templates where Jinja will look for them."""
os.makedirs("templates", exist_ok=True)
for name, body in TEMPLATES.items():
with open(os.path.join("templates", name), "w",
encoding="utf-8") as handle:
handle.write(body)
def smoke_test():
"""Exercise every GET route once, without starting a server.
`app.test_client()` dispatches a real request through the real application
object -- no socket, no port, no waiting. A web project that cannot be
driven this way cannot be tested either.
It returns the number of routes that did **not** answer 2xx, and the
caller turns that into an exit code. The earlier version printed the
status and exited 0 regardless, so this file reported success while every
content route returned 500.
"""
print("smoke test: dispatching one request per route\n")
broken = 0
with app.test_client() as client:
rules = sorted(app.url_map.iter_rules(), key=lambda rule: str(rule))
checked = 0
for rule in rules:
if "GET" not in rule.methods or rule.arguments:
continue
response = client.get(str(rule))
body = " ".join(response.get_data(as_text=True).split())[:60]
if response.status_code >= 400:
broken += 1
print(f" GET {str(rule):26} {response.status_code} {body}")
checked += 1
print(f"\n{checked} route(s) answered, {broken} failing. "
f"Pass --serve to start the real server instead.")
return broken
if __name__ == "__main__":
# Templates first: they are what the routes render, and rendering them
# before they exist is what made every content route 500.
write_templates()
# Serving is opt-in, because a run that never returns cannot be tested or
# captured. With no arguments the file answers every route once and exits.
if "--serve" in sys.argv:
app.run(debug=True)
else:
raise SystemExit(1 if smoke_test() else 0) Run it
Section titled “Run it”C:\Users\Your Name\ecommerce> python ecommerce_website.py
# Visit http://127.0.0.1:5000 in your browser.What it produces
Section titled “What it produces”Running the file exactly as it ships takes 0.5 s and prints:
smoke test: dispatching one request per route
GET / 200 <!DOCTYPE html> <html lang="en"> <head><meta charset="utf-8"
GET /cart 200 <!DOCTYPE html> <html lang="en"> <head><meta charset="utf-8"
2 route(s) answered, 0 failing. Pass --serve to start the real server instead.Step-by-Step Explanation
Section titled “Step-by-Step Explanation”1. The app and data
Section titled “1. The app and data”app = Flask(__name__)
products = [
{"id": 1, "name": "Laptop", "price": 800},
{"id": 2, "name": "Smartphone", "price": 500},
...
]
cart = []Flask(__name__) creates the app. Products are a hard-coded list of dicts (a database stand-in). cart is — for now — a single global list. (Hold that thought.)
2. Routes map URLs to functions
Section titled “2. Routes map URLs to functions”@app.route('/')
def index():
return render_template('index.html', products=products)The decorator binds the URL / to index(). render_template loads index.html and injects products so Jinja can loop over them.
3. URL parameters and redirect
Section titled “3. URL parameters and redirect”@app.route('/add_to_cart/<int:product_id>')
def add_to_cart(product_id):
product = next((p for p in products if p['id'] == product_id), None)
if product:
cart.append(product)
return redirect(url_for('view_cart'))<int:product_id> captures a number from the URL and passes it as an argument. After adding, the route redirects to the cart — the Post/Redirect/Get pattern, which stops a page refresh from re-adding the item.
4. Computing the total
Section titled “4. Computing the total”@app.route('/cart')
def view_cart():
total_price = sum(item['price'] for item in cart)
return render_template('cart.html', cart=cart, total_price=total_price)A generator expression sums the prices — clean and lazy.
The Critical Bug: One Cart for the Whole Internet
Section titled “The Critical Bug: One Cart for the Whole Internet”cart = [] lives at module level, so every visitor shares the same cart. Two users shopping at once would see each other’s items. The fix is Flask sessions — per-user storage backed by a signed cookie:
from flask import session
app.secret_key = "change-me" # required to sign the session cookie
@app.route('/add_to_cart/<int:product_id>')
def add_to_cart(product_id):
cart = session.get("cart", [])
cart.append(product_id) # store IDs, not whole objects
session["cart"] = cart
return redirect(url_for('view_cart'))Now each browser gets its own cart. This is the single most important upgrade in the project.
Add Quantities
Section titled “Add Quantities”Storing duplicate IDs is clumsy. Use a {product_id: quantity} map:
cart = session.get("cart", {})
cart[str(pid)] = cart.get(str(pid), 0) + 1 # JSON keys must be strings
session["cart"] = cartOn the cart page, multiply price × quantity per line.
Add a Checkout
Section titled “Add a Checkout”A form that captures the order and clears the cart:
@app.route('/checkout', methods=['POST'])
def checkout():
order = session.pop("cart", {})
# save the order to a database here...
return render_template('thanks.html', order=order)Move Products to a Database
Section titled “Move Products to a Database”Hard-coded products don’t scale. SQLite is the natural next step:
import sqlite3
def get_products():
conn = sqlite3.connect("shop.db")
rows = conn.execute("SELECT id, name, price FROM products").fetchall()
conn.close()
return [{"id": r[0], "name": r[1], "price": r[2]} for r in rows]Common Mistakes
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
| Everyone shares one cart | Global cart list | Use session (per-user) |
RuntimeError: secret key | Sessions without a key | Set app.secret_key |
TemplateNotFound | HTML not in templates/ | Put templates in the templates/ folder |
| Refresh re-adds the item | Action returned HTML directly | Redirect after the POST (PRG) |
| Prices wrong with duplicates | Counting items, not quantities | Store {id: qty} and multiply |
| Data lost on restart | Everything in memory | Persist products/orders to a DB |
Variations to Try
Section titled “Variations to Try”- Sessions cart — the must-do fix above.
- Quantities & remove — increment, decrement, delete lines.
- Product detail pages —
/product/<id>with description and image. - Search & categories — filter the catalog.
- User accounts — login, order history (see REST API with Authentication).
- Payment stub — integrate a sandbox Stripe checkout.
- Admin panel — add/edit products through a form.
Real-World Applications
Section titled “Real-World Applications”- Online stores — the skeleton of any Shopify-style shop.
- Booking systems — carts generalize to reservations and tickets.
- SaaS billing — plan selection and checkout flows.
- Learning Flask — the canonical “real app” beyond hello-world.
Educational Value
Section titled “Educational Value”- Web routing — URLs to handlers, URL parameters.
- Templating — Jinja loops and
url_for. - State & sessions — the difference between global and per-user state.
- App architecture — the path from in-memory demo to DB-backed app.
Next Steps
Section titled “Next Steps”- Replace the global cart with sessions.
- Add quantities, remove, and a checkout.
- Move products and orders into a database.
- Add accounts and a payment integration.
Visualize it
Section titled “Visualize it”Here’s how a request flows through the Flask app, from the browser to the database and back as rendered HTML.
flowchart LR A["Browser"] --> B["Flask routes/views"] B --> C["Business logic"] C --> D["SQLAlchemy"] D --> E["Database"] B --> F["Jinja2 templates"] F --> A
Conclusion
Section titled “Conclusion”You built a working e-commerce site — products, cart, totals — and learned the routing/templating/redirect loop at the heart of every Flask app. More importantly, you found and fixed the bug that separates a demo from a real store: shared global state versus per-user sessions. With quantities, checkout, and a database, this scales into a genuine shop. Full source on GitHub. Explore more web projects on Python Central Hub.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading