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 a Flask instance, 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 &lt;script&gt;, 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=flask with a default.
  • request.form - fields from an HTML form posted as application/x-www-form-urlencoded or multipart/form-data.
  • request.get_json() - the parsed body when the client sends Content-Type: application/json. Use request.get_json(silent=True) to get None instead 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.

Frequently Asked Questions

Is Flask still a good choice in 2026? Yes. Flask is actively maintained and remains one of the most widely used Python web frameworks. It suits APIs, small to medium apps, and services where you want to pick your own database and structure. For a large app that needs a built-in admin and ORM, Django may save time.
What is the difference between Flask and FastAPI? Flask is synchronous by default and centred on Jinja templates; FastAPI is async-first and built around type-hint validation with automatic API docs. Flask is often chosen for server-rendered pages, FastAPI for JSON APIs. Both are lightweight and unopinionated about storage.
How do I fix 'Working outside of application context'? Wrap the code in 'with app.app_context():' if it needs current_app or an extension, or 'with app.test_request_context():' if it needs request or session. In background threads, push the context inside the thread's own function.
Do I need jsonify to return JSON? Not for dicts and lists - returning them from a view produces a JSON response automatically since Flask 2.2. jsonify is still useful when you want to set a status code or headers, or serialize types it handles specially.
Can I run the Flask development server in production? No. 'flask run' and app.run() use a single-threaded server not built for load or security. Deploy behind a WSGI server such as Gunicorn or uWSGI, usually with Nginx in front: gunicorn 'app:create_app()'.