Skip to content

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_template passes data into Jinja HTML.
  • Why a module-level cartcart list 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

  1. Create a folder named ecommerceecommerce.
  2. Inside it, create ecommerce_website.pyecommerce_website.py and a templates/templates/ folder.
  3. 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:

templates/index.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>
templates/index.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>
templates/cart.html
<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>
templates/cart.html
<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.pySource
ecommerce_website.py
"""
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)
 
ecommerce_website.py
"""
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

command
C:\Users\Your Name\ecommerce> python ecommerce_website.py
# Visit http://127.0.0.1:5000 in your browser.
command
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

ecommerce_website.py
app = Flask(__name__)
products = [
    {"id": 1, "name": "Laptop", "price": 800},
    {"id": 2, "name": "Smartphone", "price": 500},
    ...
]
cart = []
ecommerce_website.py
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

ecommerce_website.py
@app.route('/')
def index():
    return render_template('index.html', products=products)
ecommerce_website.py
@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

ecommerce_website.py
@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'))
ecommerce_website.py
@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

ecommerce_website.py
@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)
ecommerce_website.py
@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:

sessions.py
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'))
sessions.py
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:

quantities.py
cart = session.get("cart", {})
cart[str(pid)] = cart.get(str(pid), 0) + 1     # JSON keys must be strings
session["cart"] = cart
quantities.py
cart = session.get("cart", {})
cart[str(pid)] = cart.get(str(pid), 0) + 1     # JSON keys must be strings
session["cart"] = cart

On the cart page, multiply price × quantity per line.

Add a Checkout

A form that captures the order and clears the cart:

checkout.py
@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)
checkout.py
@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:

db.py
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]
db.py
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

ProblemCauseFix
Everyone shares one cartGlobal cartcart listUse sessionsession (per-user)
RuntimeError: secret keyRuntimeError: secret keySessions without a keySet app.secret_keyapp.secret_key
TemplateNotFoundTemplateNotFoundHTML not in templates/templates/Put templates in the templates/templates/ folder
Refresh re-adds the itemAction returned HTML directlyRedirect after the POST (PRG)
Prices wrong with duplicatesCounting items, not quantitiesStore {id: qty}{id: qty} and multiply
Data lost on restartEverything in memoryPersist products/orders to a DB

Variations to Try

  1. Sessions cart — the must-do fix above.
  2. Quantities & remove — increment, decrement, delete lines.
  3. Product detail pages/product/<id>/product/<id> with description and image.
  4. Search & categories — filter the catalog.
  5. User accounts — login, order history (see REST API with Authentication).
  6. Payment stub — integrate a sandbox Stripe checkout.
  7. 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.

diagram E-commerce Flask architecture mermaid
A browser request travels through Flask routes and business logic to the database, then comes back as a rendered Jinja2 template.

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 coffee

Was this page helpful?

Let us know how we did