E-commerce Website (Basic)
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@app.route. - The request → process → redirect pattern (POST/Redirect/GET).
- How
render_templaterender_templatepasses data into Jinja HTML. - Why a module-level
cartcartlist is a serious bug, and how sessions fix it.
Prerequisites
- Python 3.6 or above.
- A text editor or IDE.
- Flask:
pip install flaskpip install flask. - Basic HTML.
- Comfort with functions and lists/dicts.
Getting Started
Create the project
- Create a folder named
ecommerceecommerce. - Inside it, create
ecommerce_website.pyecommerce_website.pyand atemplates/templates/folder. - Install Flask:
pip install flaskpip install flask.
Templates
Flask looks for HTML in templates/templates/. You’ll need index.htmlindex.html and cart.htmlcart.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>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><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
ecommerce_website.py
Source"""
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.
"""
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)
if __name__ == '__main__':
app.run(debug=True)
"""
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.
"""
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)
if __name__ == '__main__':
app.run(debug=True)
Run it
C:\Users\Your Name\ecommerce> python ecommerce_website.py
# Visit http://127.0.0.1:5000 in your browser.C:\Users\Your Name\ecommerce> python ecommerce_website.py
# Visit http://127.0.0.1:5000 in your browser.Step-by-Step Explanation
1. The app and data
app = Flask(__name__)
products = [
{"id": 1, "name": "Laptop", "price": 800},
{"id": 2, "name": "Smartphone", "price": 500},
...
]
cart = []app = Flask(__name__)
products = [
{"id": 1, "name": "Laptop", "price": 800},
{"id": 2, "name": "Smartphone", "price": 500},
...
]
cart = []Flask(__name__)Flask(__name__) creates the app. Products are a hard-coded list of dicts (a database stand-in). cartcart is — for now — a single global list. (Hold that thought.)
2. Routes map URLs to functions
@app.route('/')
def index():
return render_template('index.html', products=products)@app.route('/')
def index():
return render_template('index.html', products=products)The decorator binds the URL // to index()index(). render_templaterender_template loads index.htmlindex.html and injects productsproducts so Jinja can loop over them.
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'))@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><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
@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)@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
cart = []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'))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
Storing duplicate IDs is clumsy. Use a {product_id: quantity}{product_id: quantity} map:
cart = session.get("cart", {})
cart[str(pid)] = cart.get(str(pid), 0) + 1 # JSON keys must be strings
session["cart"] = cartcart = 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
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)@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
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]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
| Problem | Cause | Fix |
|---|---|---|
| Everyone shares one cart | Global cartcart list | Use sessionsession (per-user) |
RuntimeError: secret keyRuntimeError: secret key | Sessions without a key | Set app.secret_keyapp.secret_key |
TemplateNotFoundTemplateNotFound | HTML not in templates/templates/ | Put templates in the templates/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}{id: qty} and multiply |
| Data lost on restart | Everything in memory | Persist products/orders to a DB |
Variations to Try
- Sessions cart — the must-do fix above.
- Quantities & remove — increment, decrement, delete lines.
- Product detail pages —
/product/<id>/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
- 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
- Web routing — URLs to handlers, URL parameters.
- Templating — Jinja loops and
url_forurl_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
- 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
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
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.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
