Checkpoint: A Dynamic Web App

Reviewed & published by Brayan K

This checkpoint ties together everything you've learned — templates, url_for, cookies, uploads, config, contexts, hooks, and class-based views — into one small dynamic web app.

Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

Instead of new theory, you'll recap the core ideas of this section, then prove you can combine them: a build challenge with a runnable starter and full solution, a short quiz, and a recap pointing to what comes next.

What You'll Bring Together in This Checkpoint

🛠️ Build Challenge: A Tiny Dynamic App

Your task is to build one small app that combines four ideas from this section into a single working page. None of the pieces are large on their own — the skill is wiring them together so they cooperate on every request.

Start from the scaffold below. It already runs and prints output — your job is to replace the TODO comments so the counter increments and the header comes from g. Run it as you go to see your progress.

# 🛠️ BUILD CHALLENGE — starter scaffold (runs as-is; finish the TODOs)
from flask import Flask, render_template_string, make_response, request, g
from flask.views import MethodView

app = Flask(__name__)

# A tiny inline template. TODO: add a line that shows {{ visits }}.
PAGE = "<h1>{{ title }}</h1><p>Tasks: {{ tasks|length }}</p>"

@app.after_request
def add_header(response):
    # TODO: read the id off g (set it in a before_request hook) instead of a constant.
    response.headers["X-Request-ID"] = "req-001"
    return response

class HomeView(MethodView):
    def get(self):
        # TODO: read the "visits" cookie, increment it, and set it back below.
        html = render_template_string(
            PAGE,
            title="My Tasks",
            tasks=["write code", "test code", "ship it"],
        )
        resp = make_response(html)
        return resp

app.add_url_rule("/", view_func=HomeView.as_view("home"))

# Drive it with the test client, no server needed:
with app.test_client() as client:
    r = client.get("/")
    print("Status:", r.status_code)               # 200
    print("X-Request-ID:", r.headers["X-Request-ID"])  # req-001
    print("Body:", r.data.decode())               # <h1>My Tasks</h1><p>Tasks: 3</p>

Out of the box this prints a 200 status, the placeholder header, and the rendered task list. Once you finish the TODOs, the body should also report the visit number, and the header should come from g.

# ✅ FULL SOLUTION — template + cookie + hook + MethodView
from flask import Flask, render_template_string, make_response, request, g
from flask.views import MethodView

app = Flask(__name__)

# 1) A templated page: a heading, the visit count, and a task list.
PAGE = (
    "<h1>{{ title }}</h1>"
    "<p>Visits: {{ visits }}</p>"
    "<ul>{% for t in tasks %}<li>{{ t|capitalize }}</li>{% endfor %}</ul>"
)

# 3) Request hooks: stash an id on g, then add it as a header on the way out.
@app.before_request
def set_request_id():
    g.request_id = "req-001"

@app.after_request
def add_header(response):
    # after_request MUST return the response.
    response.headers["X-Request-ID"] = g.get("request_id", "?")
    return response

# 4) A class-based view: GET is dispatched to get().
class HomeView(MethodView):
    def get(self):
        # 2) Cookie visit counter (cookies are strings, so convert with int()).
        visits = int(request.cookies.get("visits", "0")) + 1
        html = render_template_string(
            PAGE,
            title="My Tasks",
            visits=visits,
            tasks=["write code", "test code", "ship it"],
        )
        resp = make_response(html)
        resp.set_cookie("visits", str(visits))
        return resp

app.add_url_rule("/", view_func=HomeView.as_view("home"))

# Drive it with the test client (it remembers cookies between calls):
with app.test_client() as client:
    r1 = client.get("/")
    print("X-Request-ID:", r1.headers["X-Request-ID"])           # X-Request-ID: req-001
    print("Visits=1 in body:", "Visits: 1" in r1.data.decode())  # True
    r2 = client.get("/")
    print("Visits=2 in body:", "Visits: 2" in r2.data.decode())  # True

🔬 How the Four Pieces Cooperate

PieceRuns whenKey call
before_request hookBefore every viewg.request_id = ...
MethodView.getOn a GET to the routerender_template_string(...)
Cookie counterInside the viewresp.set_cookie("visits", ...)
after_request hookAfter every viewresponse.headers[...] = ...

The request flows top to bottom: the before_request hook fires first, the view builds the page and cookie, and the after_request hook stamps the header just before the response leaves.

⚙️ Review: A Config Loader

Configuration is the one piece from this section the build challenge did not touch. app.config.from_object() copies only the UPPER_CASE attributes of a class, so a ProductionConfig can inherit safe defaults and override just what must change. Secrets then come from the environment, never from code. This runs as plain Python: run it and compare the three lines it prints with the two classes.

# WORKED EXAMPLE: how app.config.from_object() and an environment override
# combine, in plain Python. A config is just a dict of UPPER_CASE keys.

class Config:                    # defaults, safe for development
    DEBUG = True
    SECRET_KEY = "dev-only"
    MAX_CONTENT_LENGTH = 1024 * 1024      # uploads capped at 1 MB

class ProductionConfig(Config):  # inherits, then overrides what must change
    DEBUG = False

def from_object(obj):
    # from_object copies only the UPPER_CASE attributes, exactly like this:
    return {key: getattr(obj, key) for key in dir(obj) if key.isupper()}

env = {"SECRET_KEY": "s3cret-from-env"}   # stands in for os.environ

config = from_object(ProductionConfig)
# A secret must never live in code: prefer the environment when it is set.
config["SECRET_KEY"] = env.get("SECRET_KEY", config["SECRET_KEY"])

for key in sorted(config):
    print(f"{key} = {config[key]!r}")

# ✅ Expected output:
# DEBUG = False
# MAX_CONTENT_LENGTH = 1048576
# SECRET_KEY = 's3cret-from-env'

🎯 Your Turn: url_for, a Cookie Counter, Upload Checks

Three ideas from this section that the build challenge left out, as plain Python that runs anywhere: url_for looking a route up by its endpoint name, a cookie counter that must convert its string value before adding, and the upload extension check. Fill the three blanks, then run and compare with the expected output.

# 🎯 YOUR TURN — three ideas from this section in plain Python: url_for's
# route lookup, a cookie visit counter, and upload validation.
# Fill in the three ___ blanks, then press Run.

# 1) URL_FOR: the app keeps a map of endpoint name -> URL rule. url_for fills
#    the <parameters> in, so links never hard-code a path.
RULES = {"home": "/", "task_detail": "/tasks/<int:task_id>", "static": "/static/<path:filename>"}

def url_for(endpoint, **values):
    url = RULES[endpoint]
    for name, value in values.items():
        for converter in ("int", "path", "string"):
            url = url.replace(f"<{converter}:{name}>", str(value))
    return url

print(url_for("home"))
print(url_for(___, task_id=7))                  # 👉 replace ___ with "task_detail"

# 2) COOKIES: cookies are always strings, so convert before adding.
cookies = {}                                     # what the browser sends back

def visit(cookies):
    visits = ___(cookies.get("visits", "0")) + 1     # 👉 replace ___ with int
    cookies["visits"] = str(visits)                  # like resp.set_cookie("visits", ...)
    return visits

print("visits:", visit(cookies))
print("visits:", visit(cookies))

# 3) UPLOADS: only accept safe extensions, like the allowed_file() helper.
ALLOWED = {"png", "jpg", "pdf"}

def allowed_file(filename):
    return "." in filename and filename.rsplit(".", 1)[1].lower() in ___   # 👉 replace ___ with ALLOWED

for name in ["report.pdf", "photo.JPG", "virus.exe"]:
    print(name, "->", "accepted" if allowed_file(name) else "rejected")

# ✅ Expected output:
# /
# /tasks/7
# visits: 1
# visits: 2
# report.pdf -> accepted
# photo.JPG -> accepted
# virus.exe -> rejected

📝 Checkpoint Quiz

Try to answer each question in your head first, then expand it to check. These cover the load-bearing ideas from the whole section.

A: url_for. You pass the endpoint name (the view function's name) plus any route parameters, and Flask returns the correct URL. Because nothing is hard-coded, changing a route's path later doesn't break your links.

A: Build a response with make_response(...), then call response.set_cookie("name", "value"). You can't set a cookie on a plain string return — you need a real response object. The browser sends the cookie back on later requests, where you read it with request.cookies.get(...).

A: It must return the response object (usually the same one it received, after modifying it). If you forget the return, the hook replaces the response with None and Flask raises an error.

A: The get(self) method. A MethodView dispatches each HTTP method to the same-named method on the class — POST to post, PUT to put, and so on — which keeps a resource's logic grouped instead of branching on request.method.

A: Debug mode shows the interactive traceback (the Werkzeug debugger), which can expose source code, configuration, and even let a visitor run arbitrary code on your server. It also auto-reloads on file changes. Both are fine locally but dangerous in production, so set DEBUG = False there.

A: g is a per-request scratchpad for data you want to share across functions during a single request — a database connection, the current user, or a request id. It's created fresh for every request and discarded when the request ends, so a value set in a before_request hook is readable in the view and the after_request hook.

Watch Out For These

❌ Forgetting to return the response from after_request

The hook must return response. Without it, the response becomes None and Flask errors out. This is the single most common mistake when wiring up request hooks.

❌ Treating a cookie value as a number

Cookies are always strings. Reading request.cookies.get("visits", "0") and adding 1 directly would concatenate, so convert with int(...) first.

❓ Frequently Asked Questions

Checkpoint complete — you can build a real dynamic app!

You combined templates, a cookie, request hooks with g, and a MethodView into one working page — the same shape that powers production Flask apps. Everything from this section now fits together in your hands.

Practice quiz

Which Flask function builds URLs to your routes instead of hard-coding paths?

  • redirect
  • make_response
  • url_for
  • render_template_string

Answer: url_for. url_for builds URLs from the endpoint name plus route parameters, so changing a route's path later does not break your links.

How do you set a cookie on a response in Flask?

  • Build a response with make_response(...) and call response.set_cookie(...)
  • return a plain string with the cookie appended
  • Assign to request.cookies directly
  • Call session.set_cookie(...)

Answer: Build a response with make_response(...) and call response.set_cookie(...). You need a real response object: make_response(...) then response.set_cookie('name', 'value'); you cannot set a cookie on a plain string return.

What must an after_request handler return?

  • None
  • A status code integer
  • The request object
  • The response object it received (after modifying it)

Answer: The response object it received (after modifying it). An after_request handler must return the response object; forgetting the return replaces it with None and Flask raises an error.

When a GET request hits a MethodView, which method runs?

  • handle()
  • get(self)
  • dispatch(self)
  • index(self)

Answer: get(self). A MethodView dispatches each HTTP method to a same-named method, so a GET calls get(self), a POST calls post(self), and so on.

Why must DEBUG be False in production?

  • The interactive debugger can expose source/config and allow arbitrary code execution
  • It speeds up template rendering
  • It enables cookies
  • It is required by url_for

Answer: The interactive debugger can expose source/config and allow arbitrary code execution. Debug mode shows the Werkzeug interactive traceback, which can leak source and config and even let a visitor run arbitrary code on the server.

What is the g object used for in Flask?

  • Global config shared across all requests forever
  • A cookie storage helper
  • A per-request scratchpad for data shared across functions during one request
  • A template rendering engine

Answer: A per-request scratchpad for data shared across functions during one request. g is a per-request scratchpad created fresh each request, so a value set in before_request is readable in the view and after_request.

Since cookies are always strings, how do you safely use a visit counter cookie as a number?

  • Add 1 to it directly
  • Convert it with int(...) before adding
  • Store it as bytes
  • Use float() on the response

Answer: Convert it with int(...) before adding. Cookies are strings, so int(request.cookies.get('visits', '0')) + 1 converts before adding; adding 1 directly would concatenate.

Which Jinja2 construct loops over a list of tasks inside a template?

  • {{ for t in tasks }}
  • <for each='tasks'>
  • @for tasks
  • {% for t in tasks %} ... {% endfor %}

Answer: {% for t in tasks %} ... {% endfor %}. Jinja2 uses statement tags {% for t in tasks %} ... {% endfor %} to loop, while {{ }} is for outputting expressions.

How do you register a MethodView subclass on a URL?

  • @app.route('/') above the class
  • app.add_url_rule('/', view_func=HomeView.as_view('home'))
  • app.register(HomeView)
  • HomeView.route('/')

Answer: app.add_url_rule('/', view_func=HomeView.as_view('home')). You call as_view('endpoint') to turn the class into a view function and pass it to app.add_url_rule(path, view_func=...).

In the request lifecycle, when does the before_request hook run relative to the view?

  • After the view returns
  • Only on POST requests
  • Before every view, so g can be set for the view to read
  • Never, unless registered per route

Answer: Before every view, so g can be set for the view to read. before_request runs before every view, so an id stashed on g there is available to the view and to the after_request hook.

Continue this course

Frequently asked questions

Which function builds URLs to my routes in Flask?

url_for builds URLs by endpoint name rather than hard-coded paths. You pass the view function's name (and any route parameters) and Flask returns the correct URL — so when you change a route's path later, every link still works because nothing was hard-coded. It also handles static files via url_for('static', filename='...').

How do I set a cookie, and how do I read it back?

Cookies attach to a response, so build one with make_response(...) and call response.set_cookie('name', 'value'). The browser stores it and sends it back on later requests, where you read it with request.cookies.get('name', default). Cookies are strings, so convert numbers with int(...) when you read them, as a visit counter does.

What does an after_request handler have to return?

An after_request handler must return the response object — usually the same one it received after modifying it (for example, adding a header). If you forget the return, Flask raises an error because the hook replaced the response with None. Use after_request to add headers, set cookies globally, or log every response on its way out.

How does a MethodView dispatch an incoming request?

A MethodView routes each HTTP method to a same-named method on the class: a GET request calls get(self), a POST calls post(self), PUT calls put(self), and so on. You register it with app.add_url_rule('/path', view_func=MyView.as_view('endpoint')). This keeps the logic for one resource grouped together instead of branching on request.method inside a single function.