Part of Python for Beginners

Flask in Python - GET, POST, PUT, DELETE with curl Testing #39

Sandy LaneSandy Lane

Video: Flask in Python - GET, POST, PUT, DELETE with curl Testing #39 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Python Flask: Lightweight Web Framework

pip install flask. app = Flask(__name__). @app.route("/path") registers an endpoint. request.args for query, request.get_json() for body. jsonify(...) for JSON response. Microframework — no built-in ORM, validation, or templates beyond basics. You compose what you need.

Flask is the classic Python web framework. Where FastAPI emphasizes types and async, Flask emphasizes simplicity and flexibility — minimal core, choose-your-own-adventure for the rest.

Hello world

from flask import Flask

app = Flask(__name__)

@app.route("/")
def root():
  return "Hello, World!"

if __name__ == "__main__":
  app.run(debug=True, port=5000)

Run:

pip install flask
python app.py
# or
flask run

Open http://localhost:5000/.

@app.route("/") registers the function as a handler for that path. Default method is GET.

Returning JSON

from flask import jsonify

@app.route("/users")
def list_users():
  return jsonify(users)

@app.route("/")
def root():
  return jsonify({"message": "Welcome", "version": "1.0"})

jsonify(...) serializes a dict (or list) to JSON and sets Content-Type: application/json.

In recent Flask versions, returning a dict directly works too:

@app.route("/users")
def list_users():
  return {"users": users}    # auto-jsonified

Path parameters

@app.route("/users/<int:user_id>")
def get_user(user_id):
  if user_id in users:
    return jsonify(users[user_id])
  return jsonify({"error": "User not found"}), 404

<int:user_id> is a converter — captures an integer and passes it as the function argument. Available converters:

  • <string:name> (default) — anything except /.
  • <int:n> — integer.
  • <float:f> — float.
  • <path:p> — like string but allows /.
  • <uuid:id> — UUID.

Tuple (body, status_code) returns a custom status. (body, 404) for not found.

Query parameters

from flask import request

@app.route("/search")
def search():
  q = request.args.get("q", "")
  limit = request.args.get("limit", 5, type=int)
  results = [u for u in users.values() if q.lower() in u["name"].lower()]
  return jsonify({"query": q, "limit": limit, "results": results[:limit]})

request.args is a dict-like object of query parameters. .get(key, default, type=...) provides a default and converts.

Request body (POST)

@app.route("/items", methods=["POST"])
def create_item():
  data = request.get_json()
  if data is None or "name" not in data:
    return jsonify({"error": "name is required"}), 400

  item = {
    "id": next_id(),
    "name": data["name"],
    "price": data.get("price", 0),
  }
  items.append(item)
  return jsonify(item), 201

methods=["POST"] overrides the default GET. request.get_json() parses the JSON body — returns None if the body isn't JSON.

For form-encoded POSTs (HTML forms), use request.form.

For files, request.files.

All HTTP methods on one route

@app.route("/items/<int:item_id>", methods=["GET", "PUT", "DELETE"])
def item(item_id):
  if request.method == "GET":
    return jsonify(items[item_id])
  elif request.method == "PUT":
    items[item_id] = request.get_json()
    return jsonify(items[item_id])
  elif request.method == "DELETE":
    del items[item_id]
    return "", 204

Or split into separate functions:

@app.route("/items/<int:item_id>", methods=["GET"])
def get_item(item_id): ...

@app.route("/items/<int:item_id>", methods=["PUT"])
def update_item(item_id): ...

@app.route("/items/<int:item_id>", methods=["DELETE"])
def delete_item(item_id): ...

Either pattern works. Separate functions are easier to test.

Error handling

from flask import abort

@app.route("/items/<int:item_id>")
def get_item(item_id):
  if item_id not in items:
    abort(404, description="Item not found")
  return jsonify(items[item_id])

@app.errorhandler(404)
def not_found(error):
  return jsonify({"error": str(error.description)}), 404

@app.errorhandler(500)
def server_error(error):
  return jsonify({"error": "Internal server error"}), 500

abort(code, description=...) raises an HTTPException. Register handlers with @app.errorhandler(code).

Templates (Jinja2)

from flask import render_template

@app.route("/users/<int:user_id>")
def user_page(user_id):
  user = users[user_id]
  return render_template("user.html", user=user)

templates/user.html:

<h1>{{ user.name }}</h1>
<p>Email: {{ user.email }}</p>
{% for tag in user.tags %}
  <span>{{ tag }}</span>
{% endfor %}

Flask uses Jinja2 templates by default. {{ }} for expressions, {% %} for control flow.

For pure JSON APIs, you don't need templates. They're for HTML pages.

Sessions and cookies

from flask import session

app.secret_key = "your-secret-key"   # required for sessions

@app.route("/login", methods=["POST"])
def login():
  username = request.form["username"]
  session["user"] = username
  return redirect(url_for("dashboard"))

@app.route("/dashboard")
def dashboard():
  user = session.get("user")
  if not user:
    return redirect(url_for("login_page"))
  return f"Hello, {user}"

session is a dict-like proxy that's stored client-side as a signed cookie. Don't put sensitive data in there — it's just signed, not encrypted (use flask-session for server-side storage).

Blueprints: organizing routes

For larger apps, split into blueprints:

# users.py
from flask import Blueprint

users_bp = Blueprint("users", __name__, url_prefix="/users")

@users_bp.route("/")
def list_users(): ...

@users_bp.route("/<int:user_id>")
def get_user(user_id): ...
# app.py
from users import users_bp

app = Flask(__name__)
app.register_blueprint(users_bp)

Each blueprint is a mini-app. Use one per resource or feature.

Extensions

Flask is intentionally minimal. Common extensions:

  • Flask-SQLAlchemy — SQLAlchemy ORM.
  • Flask-Migrate — Alembic migrations.
  • Flask-Login — user sessions.
  • Flask-WTF — forms with CSRF.
  • Flask-Limiter — rate limiting.
  • Flask-Cors — CORS.

Pick what you need; ignore the rest.

A complete task API

from flask import Flask, jsonify, request

app = Flask(__name__)
tasks = {}
next_id = 1

@app.route("/tasks", methods=["POST"])
def create_task():
  global next_id
  data = request.get_json()
  if not data or "title" not in data:
    return jsonify({"error": "title is required"}), 400
  task = {
    "id": next_id,
    "title": data["title"],
    "status": "pending",
    "priority": data.get("priority", "medium"),
  }
  tasks[next_id] = task
  next_id += 1
  return jsonify(task), 201

@app.route("/tasks", methods=["GET"])
def list_tasks():
  status = request.args.get("status")
  results = list(tasks.values())
  if status:
    results = [t for t in results if t["status"] == status]
  return jsonify({"tasks": results, "count": len(results)})

@app.route("/tasks/<int:task_id>", methods=["GET"])
def get_task(task_id):
  task = tasks.get(task_id)
  if not task:
    return jsonify({"error": "Not found"}), 404
  return jsonify(task)

@app.route("/tasks/<int:task_id>", methods=["PUT"])
def update_task(task_id):
  if task_id not in tasks:
    return jsonify({"error": "Not found"}), 404
  data = request.get_json()
  for key in ["title", "status", "priority"]:
    if key in data:
      tasks[task_id][key] = data[key]
  return jsonify(tasks[task_id])

@app.route("/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
  if task_id not in tasks:
    return jsonify({"error": "Not found"}), 404
  removed = tasks.pop(task_id)
  return jsonify(removed)

if __name__ == "__main__":
  app.run(debug=True)

CRUD on tasks. ~50 lines. Clear and direct.

Testing

def test_create_task():
  client = app.test_client()
  response = client.post("/tasks", json={"title": "Buy milk"})
  assert response.status_code == 201
  assert response.json["title"] == "Buy milk"

Flask's test client runs the app in-process. Use it with pytest.

Deployment

# Don't use the built-in `flask run` for production
# Use a real WSGI server:

pip install gunicorn
gunicorn -w 4 app:app

# Or behind nginx, systemd, Docker, etc.

The flask run command is for dev only — single-threaded, no auto-restart on workers.

Flask vs FastAPI

Aspect Flask FastAPI
Sync/async Sync first; async added in 2.0+ Async-first
Type hints Optional Core feature
Validation Manual / extensions Built-in via Pydantic
Auto-docs No (extensions) Yes (/docs)
Speed Slower Faster
Ecosystem Huge, mature Newer but rich
Learning curve Gentler Type-hint-aware
Best for Small/medium apps, traditional web APIs, microservices

For a JSON API today, FastAPI is usually the better default. For a small server-rendered web app, Flask is simpler and has more templates/examples.

Common stumbles

Forgetting methods=. Default is GET only. POST/PUT/DELETE need explicit declaration.

Returning a tuple wrong. return jsonify(data), 201 is correct. return (jsonify(data), 201) works too. But return data, 201 (raw dict) only works in newer Flask.

request.get_json() returns None. When the request didn't send JSON, or the content-type is wrong. Always check.

Globals for state. tasks = {} doesn't work with multiple workers — each has its own copy. Use a database or shared store.

flask run in production. Single-threaded, slow. Use gunicorn (or uwsgi).

Mutating request.args. It's immutable. To modify, copy first.

Forgetting secret_key for sessions. Sessions silently break.

Long-running endpoints. Block the worker. Use Celery, RQ, or background workers for heavy tasks.

What's next

Lesson 40: packaging with pyproject.toml. Building distributable Python packages.

Recap

@app.route("/path") registers an endpoint. <int:name> for typed path parameters. request.args for query, request.get_json() for body. jsonify(...) for JSON responses; tuple (body, status) for custom codes. methods=["GET", "POST"] for HTTP method routing. Use blueprints to organize larger apps. For sessions, set app.secret_key. Run via gunicorn in production, not the dev server.

Next lesson: pyproject.toml packaging.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.