What you'll learn
Quick Answer
Flask is a lightweight Python web framework. It provides URL routing, request parsing, and the Jinja2 template engine, and leaves database choice, form validation, and project structure to you. A minimal app is a single file: create aFlaskinstance, attach view functions with@app.route, and run the development server.
A working app in one file
A Flask app is a Flask instance with view functions attached by decorator. The __name__ argument tells Flask where the app lives so it can find templates and static files. Save this as app.py:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello, Priodemy!"
Run it with the CLI:
pip install flask
flask --app app run --debug
Visit http://127.0.0.1:5000 and you get the string back as an HTML response - whatever a view returns becomes the response body. The --debug flag turns on the auto-reloader and the in-browser traceback page. That debugger is a real convenience and a real hazard: it can execute arbitrary Python through the browser if someone reaches it, so it must never be enabled on a public deployment. The older app.run(debug=True) call still works, but the CLI is now the standard way to start the development server.
Routes, dynamic URLs, and methods
Route patterns can capture parts of the URL. A converter in angle brackets both extracts the value and constrains its type:
@app.route("/users/<int:user_id>")
def get_user(user_id):
return {"id": user_id, "type": type(user_id).__name__}
/users/42 passes user_id as the integer 42. /users/abc does not match this rule at all and returns 404 - the int converter rejected it before your function ran. Other converters include string (the default), slug, path, and uuid.
By default a route answers only GET (plus HEAD and OPTIONS). To accept form submissions, list the methods and branch on request.method:
from flask import request
@app.route("/echo", methods=["GET", "POST"])
def echo():
if request.method == "POST":
return {"received": request.form.to_dict()}
return {"method": "GET"}
Return a dict or a list and Flask serializes it to JSON with the right content type - no jsonify call needed since Flask 2.2.
Templates with Jinja2
Returning HTML strings gets unwieldy fast. Flask ships Jinja2 and looks for templates in a templates/ folder next to your app. A view renders one with render_template:
from flask import render_template
@app.route("/greet/<name>")
def greet(name):
return render_template("greet.html", name=name)
And templates/greet.html:
<h1>Hi {{ name }}</h1>
{% if name == "admin" %}<p>You have access.</p>{% endif %}
The double-brace {{ name }} prints a value; {% ... %} is a statement such as a loop or conditional. Crucially, Jinja auto-escapes values in .html templates. Pass <script>alert(1)</script> as name and the page shows that text literally - it is rendered as <script>, not executed. This is your main protection against cross-site scripting, so avoid the | safe filter and Markup() unless you completely trust the content.
Reading query strings, forms, and JSON
The request object holds everything about the incoming request. The three sources you touch most:
request.args- the query string.request.args.get("q", "")reads?q=flaskwith a default.request.form- fields from an HTML form posted asapplication/x-www-form-urlencodedormultipart/form-data.request.get_json()- the parsed body when the client sendsContent-Type: application/json. Userequest.get_json(silent=True)to getNoneinstead of a 400 on malformed JSON.
Prefer .get() over bracket access. request.form["email"] raises a 400 Bad Request when the field is missing; request.form.get("email") returns None so you can validate and respond with your own message. File uploads live in request.files. For anything user-facing, pair this with a library like WTForms - Flask deliberately leaves form validation and CSRF protection to extensions rather than building them in.
The application context error
Sooner or later you will hit this:
RuntimeError: Working outside of application context.
It happens when code touches something app- or request-bound while no request is being handled - commonly calling current_app.config, url_for, or a database helper from a script, a background thread, or the Python shell.
Flask keeps current_app, g, request, and session in context-local storage that is only populated while a request is in flight, or while you have pushed a context yourself. During a normal request everything is set up for you. Outside one, you push it manually:
with app.app_context():
print(current_app.name) # works now
with app.test_request_context("/search?q=hi"):
print(request.args.get("q")) # "hi"
The related message Working outside of request context means the same thing for request and session specifically. If you see either inside a thread you started, that thread did not inherit the context - push one at the top of the thread's function.
Blueprints and the app factory
One file is fine while you learn. Past a handful of routes, split the app into blueprints - self-contained groups of routes that get registered onto the app:
from flask import Blueprint, jsonify
api = Blueprint("api", __name__, url_prefix="/api")
@api.route("/ping")
def ping():
return jsonify(pong=True)
Then assemble the app in a factory function rather than at module top level:
def create_app():
app = Flask(__name__)
app.register_blueprint(api)
return app
Now /api/ping works and the route lives in its own module. The factory pattern matters for testing: each test can build a fresh app with its own config, and there is no import-time side effect creating a half-configured global. Run it with flask --app "app:create_app" run. Blueprints plus a factory is the structure most production Flask projects converge on.
