Getting Started
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Flask 3.1.3 (released 2026-02-18), Werkzeug 3.1.9 and Click 8.5.0, checked on 2026-10-07.
Flask 3.1 supports Python 3.9 and newer; the code on this page uses Python 3.10+ syntax and was executed on
Python 3.13. Flask 3.2 has not been released yet, so everything described as "Flask 3.2" below is upcoming and may
still change. The page takes you from an empty directory to a running app and explains what each step does and which
library does the work underneath: Werkzeug runs the development server and the debugger, Click provides the flask
command, and python-dotenv (optional) loads .env files. The app is the first version of the Bookshelf that grows
over the following pages of this section.
Create a Project and a Virtual Environment
Every Python project needs its own virtual environment so that its packages do not collide with those of other
projects. The mechanics of virtual environments, package managers and pyproject.toml are covered in the Python
Reference, in Virtual Environments & Packaging,
and are not repeated here. The two common ways to get a Flask project going are the standard library and
uv.
With the standard library, as in the official installation guide:
mkdir bookshelf
cd bookshelf
python3 -m venv .venv
. .venv/bin/activate # Windows: .venv\Scripts\activate
pip install Flask
Source: Flask: Installation
With uv, which creates the environment in .venv for you, records the versions it resolved in uv.lock and lets you
run commands without activating anything:
uv init bookshelf --bare
cd bookshelf
uv add flask
uv run flask --version
Source: Flask: Installation
flask --version prints the versions of Python, Flask and Werkzeug, which is the first thing to paste into a bug
report:
Python 3.13.15
Flask 3.1.3
Werkzeug 3.1.9
Inside Python the supported way to read the installed version is the package metadata. The __version__ attribute
of the flask module has been deprecated since Flask 3.0 and Flask 3.2 removes it:
from importlib.metadata import version
print(version("flask"), version("werkzeug"))
Source: Flask: Changes
What pip install Flask Installs
Flask is a thin layer over the Pallets libraries. Installing it pulls in six distributions:
| Distribution | Role in Flask |
|---|---|
Werkzeug |
The WSGI toolkit: request and response objects, URL routing, the development server, the interactive debugger and the reloader, password hashing helpers and the test client. |
Jinja |
The template language behind |
MarkupSafe |
Comes with Jinja; its |
ItsDangerous |
Signs data cryptographically; it protects the session cookie. |
Click |
Builds command-line programs; it provides the |
Blinker |
Implements the signals Flask sends at points of the request cycle. |
Optional Extras
Three optional packages change how the development tools behave. They are not installed by default, and Flask uses them only when it finds them:
| Install | Provides | What changes |
|---|---|---|
|
python-dotenv |
The |
|
asgiref |
Views, hooks and error handlers may be coroutine functions ( |
|
watchdog |
A faster, more efficient reloader for the development server. Without it the reloader polls file modification times ("stat" reloader). |
The extras are declared in the package metadata: flask declares async (asgiref) and dotenv (python-dotenv),
while watchdog is an extra of werkzeug, not of flask. A combined development install, with the quotes that stop
zsh from expanding the square brackets, is:
uv add "flask[async,dotenv]" "werkzeug[watchdog]"
# or, with pip:
pip install "flask[async,dotenv]" "werkzeug[watchdog]"
If greenlet-based concurrency is what you want (gevent), that is a different path covered on the async pages of this
section; Flask’s own async extra only runs coroutine views on a per-request event loop.
The Minimal App
Save the following as bookshelf.py. Do not name the file flask.py: it would shadow the package and break
from flask import Flask.
from flask import Flask, request
from markupsafe import escape
app = Flask(__name__)
@app.route("/")
def index():
return "<p>Hello, Bookshelf!</p>"
@app.route("/hello")
def hello():
name = request.args.get("name", "reader")
return f"<p>Hello, {escape(name)}!</p>"
client = app.test_client()
response = client.get("/")
print(response.status_code, response.content_type)
print(response.get_data(as_text=True))
What the code does:
-
Flask(__name__)creates the WSGI application object. The first argument is the import name of the module or package. Flask uses it to locate resources that live next to the code, such astemplates/andstatic/, so__name__is the right value for almost every app. -
@app.route("/")registers the function below it as the view for the URL rule/. The rule is stored in Werkzeug’s routing map. By default a rule answersGET, and Flask addsHEADandOPTIONSautomatically. -
A view returns the body. A string becomes an HTML response with status 200; a dict or list becomes JSON; tuples add a status or headers. The full conversion rules belong to the responses pages of this section.
-
app.test_client()returns a Werkzeug test client that calls the app in process, without sockets. It is the quickest way to try a view and the basis of the testing page later in this section.
Running the block prints:
200 text/html; charset=utf-8
<p>Hello, Bookshelf!</p>
Application Discovery with flask --app
The flask command is installed by Flask, not by your project, so it needs to be told which application to load. The
option is --app (short form -A) and takes an import string. It has three optional parts: a directory (the
working directory is changed to it), a module path or Python file, and the name of an app or factory.
| Value | Meaning |
|---|---|
(nothing) |
Import |
|
Import module |
|
Change the working directory to |
|
Import the dotted path |
|
Use the Flask instance named |
|
Call the factory |
Detection inside the imported module follows a fixed order: first an instance named app or application, then
any single Flask instance (two instances are an error that asks for module:name), and finally a function named
create_app or make_app, which is called without arguments. The application factory pattern and why you would want
it have their own page, Application Factory & Project Layout.
flask routes is the easiest command to check that discovery worked, because it lists the URL rules of the loaded
app:
flask --app bookshelf routes
Source: Flask: Command Line Interface
Endpoint Methods Rule
-------- ------- -----------------------
hello GET /hello
index GET /
static GET /static/<path:filename>
The same command can be run in process with the CLI test runner, which is how the output above can be checked in a
test. app.test_cli_runner() invokes the commands of app.cli by default; passing a FlaskGroup that wraps the app
gives access to the built-in commands such as routes as well:
from flask.cli import FlaskGroup
result = app.test_cli_runner().invoke(cli=FlaskGroup(create_app=lambda: app), args=["routes"])
print(result.output)
If nothing matches, the command stops with Error: Could not locate a Flask application, and a module that imports
but contains no instance or factory gives Failed to find Flask application or factory in module. Typical causes are
a misspelled module name, running the command from a different directory than the one that holds the file, or a
module that defines the app inside a function that is not called create_app or make_app.
The command can also be started as python -m flask --app bookshelf run, which is handy when the flask script is not
on PATH or when you want to be sure which interpreter is used.
The Development Server
flask --app bookshelf run
Source: Flask: Development Server
* Serving Flask app 'bookshelf'
* Debug mode: off
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
The log also contains a line that warns you the development server must not be used in production (omitted from the
samples on this page). The server is Werkzeug’s run_simple. It handles requests in threads by default, binds to 127.0.0.1 so that only your
own machine can reach it, and is explicitly not meant for production: it is not designed to be efficient, stable or
secure. The production servers are the topic of the deployment pages of this section.
Open http://127.0.0.1:5000/ in a browser, or call it from a second terminal as shown in Calling the App.
flask run Options
| Option | Effect |
|---|---|
|
Switch debug mode on or off. Also valid before the command: |
|
Interface to bind to. Default |
|
Port to bind to. Default |
|
Serve HTTPS. |
|
Switch the reloader. The default follows debug mode. |
|
Switch the interactive debugger. The default follows debug mode. |
|
Switch multithreading of the server. On by default. |
|
Extra files that trigger a reload when they change; several paths are separated with |
|
|
The list matches the output of flask run --help for Flask 3.1.3. Any option can also be set from the environment
with the pattern FLASK_COMMAND_OPTION, for example FLASK_RUN_PORT=8000; this is how a .flaskenv file sets
defaults (see Environment Variables from .env and .flaskenv).
Starting the Server from Python
Flask.run() takes the same ideas as keyword arguments (host, port, debug and the Werkzeug options such as
use_reloader, use_debugger, extra_files, exclude_patterns and passthrough_errors). Keep it behind a main
guard so that importing the module from a production server does not start a second server:
# norun
if __name__ == "__main__":
app.run(debug=True)
Three differences from flask run matter. python bookshelf.py is not the same as flask --app bookshelf run: with
app.run() the server crashes if a reload meets a syntax error, whereas flask run keeps serving and shows the error in
the debugger. If SERVER_NAME is configured, app.run() takes its default host and port from it. And a call to
app.run() made while flask run is in charge is ignored with a message, so a leftover app.run() without a main
guard does not start two servers.
Debug Mode
Debug mode is the setting that turns Flask into a comfortable development tool, and the one that must never reach
production. Turn it on with --debug:
flask --app bookshelf run --debug
Source: Flask: Quickstart, Debug Mode
* Serving Flask app 'bookshelf'
* Debug mode: on
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with watchdog (fsevents)
* Debugger is active!
* Debugger PIN: 228-665-527
Debug mode sets app.debug, which is the DEBUG configuration key, and in flask run it switches on two Werkzeug
features by default: the reloader and the interactive debugger. It also makes Flask let unhandled exceptions
propagate to the server instead of turning them into a plain 500 response, and enables template auto-reload.
The environment variable behind the option is FLASK_DEBUG. Any value except 0, false, no or an empty value
turns debug mode on. The sources are applied in this order: the debug argument of app.run() overrides everything
else; the --debug option sets FLASK_DEBUG itself before the app loads, so a factory can read it; otherwise the
environment variable decides.
The effect on exceptions is easy to see in a test, using two apps that differ only in debug:
def make_app(debug: bool) -> Flask:
demo = Flask("demo")
demo.debug = debug
@demo.route("/boom")
def boom():
raise RuntimeError("simulated failure")
return demo
production_like = make_app(debug=False)
print(production_like.test_client().get("/boom").status_code)
development = make_app(debug=True)
try:
development.test_client().get("/boom")
except RuntimeError as exc:
print("propagated:", exc)
The first app answers with 500; the second re-raises the exception, which is what lets the interactive debugger (or
your test runner) see the real traceback. Flask also logs the traceback of the first one; that log output is
expected.
The Reloader
With the reloader on, Werkzeug restarts the server process whenever a Python file your app imported changes. The
flask run command starts a child process that runs the app and a parent that watches it:
-
If watchdog is installed, the reloader uses it, and the startup message says
Restarting with watchdog (fsevents)(the name of the backend depends on the operating system). Without it the message saysRestarting with statand modification times are polled. -
--extra-filesadds files that are not imported modules, for example a TOML settings file or a translation catalog, and--exclude-patternsremoves files and directories that should be ignored. When a watched file changes the log showsDetected change in '<path>', reloading. -
Errors on reload are deferred: if you save a syntax error while the server runs, the server keeps running and the next request shows the error in the debugger. An error that already exists when you start
flask runstops it at once with the traceback.
flask --app bookshelf run --debug --extra-files settings.toml --exclude-patterns '*.log'
Editing settings.toml while this runs prints Detected change in '.../settings.toml', reloading and restarts the
server; writing to a .log file does not. The same behaviour is available from code as the extra_files and
exclude_patterns arguments of app.run().
The reloader restarts the process, so state kept in memory (caches, in-process sessions, a global counter) is lost on each reload. That is a feature in development and a reason never to rely on module globals for data.
The Interactive Debugger and Its PIN
When a view raises an exception in debug mode, the server answers with an HTML page that shows the traceback. Each frame of the stack can open a console in that frame’s scope, which lets you inspect variables and run Python code in the server process. That is exactly why the debugger is a remote code execution feature, and why the official documentation says in bold not to run the development server or the built-in debugger in production.
To make it harder to abuse, the console is protected by a PIN. The PIN is printed in the server log
(Debugger PIN: 228-665-527) and is derived in a stable way from facts about the machine and the project, so it stays
the same across reloads. Entering it in the browser unlocks the consoles. The relevant details from the Werkzeug
documentation:
-
WERKZEUG_DEBUG_PINset to a number chooses the PIN (digits, with or without hyphens); set tooffit disables the PIN check. Do not do this on any machine other than your own. -
Too many wrong guesses lock the debugger until the server restarts.
-
The console only answers requests whose
Hostis a trusted one:localhost, a.localhostsubdomain or127.0.0.1by default. -
The PIN is explicitly "not meant to entirely secure the debugger" but to make attacks harder. Treat the debugger as dangerous even with the PIN.
export WERKZEUG_DEBUG_PIN=123-456-789 # a fixed PIN for local work only
flask --app bookshelf run --debug
Source: Werkzeug: Debugging Applications
Werkzeug accepts digits with or without hyphens as an explicit PIN; any other value is ignored and a PIN is generated as usual.
External Debuggers
IDE debuggers (PyCharm, VS Code) can step through a request before an error happens, which the built-in debugger cannot. Keep debug mode on so that Flask does not hide unhandled errors behind a generic 500 page, but switch off the two Werkzeug features that would compete with the IDE:
flask --app bookshelf run --debug --no-debugger --no-reload
With the built-in debugger off, the development server still catches unhandled exceptions and logs the traceback,
answering 500, so it does not crash on the first error. Pass passthrough_errors=True to app.run() only if you
really want the exception to reach your debugger and stop the server; the documentation notes that this is usually not
what you want. In PyCharm, create a run configuration with module flask and the parameters
--app bookshelf run --debug --no-debugger --no-reload.
What Debug Mode Must Never Touch
| Never | Why |
|---|---|
|
Anyone who gets past the PIN executes Python code as the user running the app, and tracebacks leak source and configuration. |
|
It publishes the debugger to the whole network. Flask binds to |
|
The development server is single-purpose tooling. Use Gunicorn or Waitress and read |
A committed |
The setting travels with the repository into deployments that load |
In production, use error-logging tools and logging as described on the debugging and logging pages, not a debugger.
Host and Port
By default the server listens on 127.0.0.1:5000. Change either with options or environment variables:
flask --app bookshelf run --port 8000
flask --app bookshelf run --host 0.0.0.0 --port 8000 # reachable from the network
FLASK_RUN_PORT=8000 flask --app bookshelf run # same, from the environment
--host=0.0.0.0 listens on all interfaces, so phones and other computers can reach the app. Do it only when the debugger
is off or when you trust everyone on the network.
Port 5000 Is Already in Use
If the port is taken the server stops, with a message like this (Flask 3.1.3, Werkzeug 3.1.9):
Address already in use
Port 5000 is in use by another program. Either identify and stop that program, or start the server with a different port.
Older versions show OSError: [Errno 98] Address already in use or, on Windows, OSError: [WinError 10013]. Either
find the process (lsof -P -i :5000 on macOS and Linux, netstat -ano | findstr 5000 on Windows) and stop it, or
pick another port with --port.
On macOS, Monterey and later start a service that listens on port 5000: the AirPlay Receiver. A fresh Flask project on
a Mac is therefore very likely to hit this error. You can either use another port (--port 5001) or switch the
service off by searching for "AirPlay Receiver" in System Settings. Setting FLASK_RUN_PORT in .flaskenv makes the
choice stick for the project, which is the better habit.
Environment Variables from .env and .flaskenv
Typing --app bookshelf --debug --port 8000 every time is tedious. When python-dotenv is installed (the dotenv
extra), the flask command and app.run() read dotenv files and set environment variables from them:
| File | Purpose |
|---|---|
|
Public defaults that belong in the repository: |
|
Private values (secret key, database URL, tokens). It must not be committed. |
a file given with |
An extra file for one run, for example |
A project could use them like this. Notice that the secret key does not appear in the repository files; it is generated once, locally:
# .flaskenv (committed)
FLASK_APP=bookshelf
FLASK_RUN_PORT=5001
# .env (listed in .gitignore, never committed)
FLASK_DEBUG=1
FLASK_SECRET_KEY=...paste the output of the command below...
python -c 'import secrets; print(secrets.token_hex())'
Source: Flask: Quickstart, Sessions
The precedence rules, from highest to lowest, are:
-
a variable already set in the real environment (including one set on the command line);
-
a file given with
-e; -
.env; -
.flaskenv.
Flask only fills in keys that are not already in os.environ, and it searches for the files upwards from the
directory where you call flask. The following example demonstrates the precedence with the same function the
command uses, flask.cli.load_dotenv, inside a throwaway directory:
import os
import tempfile
from pathlib import Path
from flask.cli import load_dotenv
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / ".flaskenv").write_text("BOOKSHELF_A=flaskenv\nBOOKSHELF_B=flaskenv\nBOOKSHELF_C=flaskenv\nBOOKSHELF_D=flaskenv\n")
(root / ".env").write_text("BOOKSHELF_B=env\nBOOKSHELF_C=env\nBOOKSHELF_D=env\n")
(root / "local.env").write_text("BOOKSHELF_C=local\nBOOKSHELF_D=local\n")
os.environ["BOOKSHELF_D"] = "real environment"
previous = Path.cwd()
os.chdir(root)
try:
load_dotenv("local.env")
for key in ("BOOKSHELF_A", "BOOKSHELF_B", "BOOKSHELF_C", "BOOKSHELF_D"):
print(key, "=", os.environ[key])
finally:
os.chdir(previous)
for key in ("BOOKSHELF_A", "BOOKSHELF_B", "BOOKSHELF_C", "BOOKSHELF_D"):
os.environ.pop(key, None)
The four lines printed show the winner of each level: BOOKSHELF_A = flaskenv, BOOKSHELF_B = env,
BOOKSHELF_C = local and BOOKSHELF_D = real environment.
More details worth knowing:
-
Before Flask 3.1, an explicit
-efile did not take precedence over.envand.flaskenv; since 3.1 it does, andload_dotenv(path, load_defaults=False)loads only the given file. -
Since Flask 2.0 the current directory is no longer changed to the directory of the loaded file, and files are read as UTF-8.
-
The files are loaded by the
flaskcommand and byapp.run()only. A production server (Gunicorn, Waitress) does not read them. Either callflask.cli.load_dotenv()yourself in the WSGI entry point, or, better in production, let the platform provide real environment variables. -
FLASK_SKIP_DOTENV=1disables loading even when python-dotenv is installed, for instance when a project runner has already loaded the files. -
Any variable named
FLASK_followed by a key can be pulled into the app configuration withapp.config.from_prefixed_env(), which strips the prefix. That is howFLASK_SECRET_KEYabove becomesSECRET_KEY. The configuration page of this section covers it fully.
import os
import secrets
os.environ["FLASK_SECRET_KEY"] = secrets.token_hex() # stands in for the real environment
config_demo = Flask("config_demo")
config_demo.config.from_prefixed_env()
print(len(config_demo.config["SECRET_KEY"]))
del os.environ["FLASK_SECRET_KEY"]
Without python-dotenv you can still set variables by exporting them in the activate script of the virtual
environment, but the documentation prefers dotenv because .flaskenv can be committed and works wherever the
project is checked out.
HTML Escaping
A view that returns a string returns HTML. Any value that came from a user (a query parameter, a form field, a path segment, a database row that a user edited) must be escaped before it is put into that HTML, or an attacker can inject markup and script into your pages. This is cross-site scripting (XSS). In templates, Jinja escapes for you (covered on the template pages), but in a string returned from a view you must do it yourself with MarkupSafe:
attack = "<script>alert(1)</script>"
safe = client.get("/hello", query_string={"name": attack})
print(safe.get_data(as_text=True))
Source: Flask: Quickstart, HTML Escaping
<p>Hello, <script>alert(1)</script>!</p>
escape is imported from markupsafe. The aliases flask.escape and flask.Markup were deprecated in Flask 2.3 and
are gone in Flask 3.0, as is from jinja2 import Markup in Jinja 3.1; books and tutorials that still use these
imports fail with ImportError today. Markup marks text as already safe, so wrapping user input in it, or using the
safe filter on it, switches the protection off. Use it only on text you produced yourself.
Calling the App
With the server running (see The Development Server), any HTTP client works. curl is installed almost everywhere:
curl -i http://127.0.0.1:5000/
curl "http://127.0.0.1:5000/hello?name=Ada"
curl --get --data-urlencode "name=<b>Ada</b>" http://127.0.0.1:5000/hello
HTTP/1.1 200 OK
Server: Werkzeug/3.1.9 Python/3.13.15
Date: Wed, 07 Oct 2026 13:02:29 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 24
Connection: close
<p>Hello, Bookshelf!</p>
The Server header shows the development server; a production deployment hides or replaces it. The third curl
shows escaping at work and prints <p>Hello, <b>Ada</b>!</p>.
HTTPie is friendlier for JSON APIs later in the section. Its == syntax adds a query-string
parameter:
http :5000/hello name==Ada
The HTTPie command above is shown for reference and was not run on this page’s check; the curl commands were run
against a live flask run server.
Async Views
With the async extra, a view, hook or error handler may be a coroutine function. Flask starts an event loop in a
worker, runs the view there and returns the result:
import asyncio
async_app = Flask("async_demo")
@async_app.route("/slow")
async def slow():
await asyncio.sleep(0.01)
return {"status": "done"}
async_client = async_app.test_client()
print(async_client.get("/slow").get_json())
Source: Flask: Using async and await
Without flask[async] the same code raises a RuntimeError that asks you to install the extra. Async views do not
make a WSGI app faster: each request still occupies one worker for its whole duration, and tasks created with
asyncio.create_task that are still running when the view returns are cancelled. The page on async, gevent and Quart
in this section explains when to choose each.
The Development Loop
The previous sections add up to a short loop. This is what happens between saving a file and seeing the result:
Source: Flask: Development Server
Before the loop starts, the flask command prepares the app in a fixed order, which explains most startup problems:
Source: Flask: Command Line Interface
Reading Companion: The Flaskr Tutorial
The official documentation contains a complete tutorial, Flaskr, a small blog with registration, login, a database,
a blueprint per feature, tests and packaging. It is the best companion to this section’s pages if you prefer to read
a finished project: it starts with the same flask --app flaskr run --debug loop, uses the application factory,
SQLite and Jinja, installs the project with pip install -e . and finishes with tests and deployment. The Bookshelf
here is a different, original application, so you can follow both without duplicating work. The tutorial is at
Flask: Tutorial.
Pitfalls
| Symptom | Cause and fix |
|---|---|
|
The project contains a file or folder called |
|
No |
|
The module has no instance named |
|
More than one Flask instance at module level. Use |
Changes are not picked up |
Debug mode is off, the file is outside the watched modules (use |
|
python-dotenv is not installed. Install |
The server answers on |
It binds to |
Everything works in development and breaks under Gunicorn |
|
Source: Flask: Command Line Interface
Security Notes for This Page
-
The debugger and the development server are for your own machine only; the PIN does not change that.
-
Never commit
.env; put secrets in it or in real environment variables and generate them withsecrets.token_hex(). The examples on this page contain no real key. -
Escape every untrusted value you put into an HTML string, and prefer templates, which escape by default.
-
Keep
--hostat the default unless you have a reason, and never combine0.0.0.0with debug mode. -
Pin your dependencies (
uv.lockor arequirementsfile with versions): Flask 3.1.1 to 3.1.3 were security releases, so staying current matters.
Where Older Books and Tutorials Are Outdated
Most Flask books and tutorials were written for versions before 2.2 and show commands that either still work in a different form or fail today. These are the items that touch getting started; the complete table of old and new code is on the What’s Changed & Migration page of this section.
| Older idiom you will meet | Status | Use instead |
|---|---|---|
|
Still works. |
|
|
Deprecated in 2.2.0, removed in 2.3.0 (2023-04-25). Setting it silently does nothing. |
|
|
Ignored since 2.3. |
Replace it with |
|
Removed in 2.2.0. |
Nothing: the app is loaded eagerly once and lazily on reload, and errors are shown immediately. |
|
No longer needed since 2.2.0. |
Plain |
|
Works, but it is unsafe. |
A production WSGI server (Gunicorn, Waitress, mod_wsgi, uWSGI) and no debugger. |
|
Legacy packaging. |
|
virtualenvwrapper |
Still works, rarely needed. |
|
|
Removed (Flask 3.0 and Jinja 3.1). |
|
|
Deprecated in 3.0, removed in Flask 3.2 (unreleased). |
|
Source: Flask: Changes
The Flask Framework Cookbook (both editions) opens with virtual environments, flask run with FLASK_APP, and a
configuration chapter that installs the app with setup.py; the Full-Stack Flask and React book starts with a
virtual environment, a .flaskenv file and flask run, but its .flaskenv and its Dockerfiles use FLASK_ENV, which
is removed in the Flask version that book targets. The commands on this page replace each of those. The configuration
loaders (from_pyfile, from_object, from_envvar, class-based configs), the static and instance folders,
blueprints and installable packages from the same cookbook chapter belong to later pages of this section.
Recent Changes That Affect Getting Started
| Version | Change |
|---|---|
2.2.0 (2022-08-01) |
|
2.2.3 (2023-02-15) |
|
2.3.0 (2023-04-25) |
|
3.0.0 (2023-09-30) |
|
3.1.0 (2024-11-13) |
Python 3.8 dropped. |
3.1.1 (2025-05-13) |
|
3.2.0 (unreleased) |
Python 3.9 dropped and |
Source: Flask: Changes
Where to Go Next
The following pages of this section build on this one: the introduction and architecture page explains what Flask, Werkzeug and Jinja are responsible for; the application factory and configuration pages move this single file into a package with environment-specific settings; and the routing and request pages grow the two example views into the Bookshelf API.