LearnThatStack Ace your next interview

Flask

Flask 2.3+ with SQLAlchemy 2.0 style Checked Nov 2026 136 code snippets

The model

  • Flask is a small core on Werkzeug, for requests and routing, and Jinja, for templates. You add the database, forms and login with extensions.
  • A WSGI server such as Gunicorn calls the Flask object once per request. The built-in server is only for your own machine.
  • request, session, current_app and g look global, but they are context locals. Each worker sees only its own request.
  • Large apps build the app in a create_app() factory, put features in blueprints and bind extensions with init_app().
WSGI serverGunicornContextsapp, then requestRequest hooksbefore and afterViewyour functionINenvironbefore_requestcall the viewOUTreturn valueafter_requeststatus and bodyENDteardown, then pop
A before_request function that returns a value skips the view. Teardown runs even after an unhandled error, so cleanup goes there.

How Flask runs a request

what Flask is, WSGI, contexts and hooks

App and request contexts

appcurrent_app, grequestrequestsessionscriptapp onlytime

A request pushes the app context, then the request context, and pops them in reverse. A script pushes the app context alone.

Get an app context in a scriptwith app.app_context():
Explain why request looks globalIt's a proxy built on contextvars.

TrapWith no active context, these names raise RuntimeError. A thread you start can't read the request thread's request.

WSGI, hooks and middleware

Define WSGIapp(environ, start_response)The Python standard for how a web server calls an app.
Run code before each view@app.before_requestReturn a value to skip the view.
Change each response@app.after_requestIt must return the response.
Clean up, even after an error@app.teardown_request@app.teardown_appcontext
Wrap the app before Flask sees the requestapp.wsgi_app = Middleware(app.wsgi_app)

TrapIf an after_request function raises, Flask skips the remaining ones. Cleanup belongs in teardown.

Routes, requests & responses

map URLs, read input, send output

Routes and HTTP methods

Accept more than GET, then branch on the methodmethods=['GET', 'POST']request.method == 'POST'
Give each method its own view@app.post('/users')
Map HTTP methods to a classclass Users(MethodView):
Register a route from a loopapp.add_url_rule('/x', 'x', view)Do it during setup. In a loop, write the view as lambda e=e:.

TrapA route with no methods answers only GET, plus HEAD and OPTIONS. Other methods get 405.

Read the request

Read a query parameter, or all its valuesrequest.args.get('page', 1, type=int)request.args.getlist('tag')
Read a form fieldrequest.form.get('email', '')
Parse a JSON bodyrequest.get_json()A non-JSON request gets 415, or None with silent=True.

Traprequest.args['q'] raises KeyError for a missing key, and Flask turns that into 400. Prefer get().

Send a response

Return JSON, with a status if you need onereturn {'ok': True}, 201jsonify(data)
Redirect, or stop with an HTTP errorredirect(url_for('index'))abort(404)
Send a file as a downloadsend_file(path, as_attachment=True)
Add a header or a cookieresp = make_response(body)resp.set_cookie('k', 'v')

TrapFlask raises a TypeError if a view returns None or ends without a return.

Upload a file

Read the file and clean its namef = request.files['file']secure_filename(f.filename)The client controls the name.
Cap the request sizeMAX_CONTENT_LENGTHLarger uploads get 413.

TrapThe form must use enctype="multipart/form-data", or the browser sends no file at all.

Templates & forms

render pages and accept input safely

Jinja syntax and inheritance

Write logic or a reusable macro{% if user %}{% macro field(name) %}
Apply a filter, or register your own{{ name|upper }}@app.template_filter('money')
Mark a block in the base, then extend it{% block content %}{% extends "base.html" %}
Add to a block instead of replacing it{{ super() }}

TrapAutoescaping prevents most cross-site scripting (XSS), not all. |safe and unquoted attributes can still let a script through.

Handle a plain form

Serve and read a form in one routeGET renders the form, and POST reads request.form.
Stop a refresh from sending the form againredirect(url_for('thanks'))This is Post/Redirect/Get.
Show a message on the next pageflash('Saved', 'success')get_flashed_messages()

Trapflash() uses the session, so it needs a SECRET_KEY. A message too large for the cookie fails silently.

Flask-WTF and CSRF

Declare a form, then validate it on submitclass Login(FlaskForm):form.validate_on_submit()
Add a rule for one fielddef validate_email(self, f):raise ValidationError('Taken')
Check cross-site request forgery (CSRF) tokensCSRFProtect(app)A missing or wrong token gets 400.
Send the token with each request{{ form.hidden_tag() }}JavaScript sends the X-CSRFToken header.

TrapA uniqueness check still needs a unique constraint. Two requests can both pass before either saves.

Sessions, login & tokens

know who is calling, and what they may do

Sessions and where they live

Store, read and remove a valuesession['user'] = namesession.get('user')session.pop('user', None)
Keep it on the server with Flask-SessionSESSION_TYPE = 'redis'The cookie holds only an ID, and all servers share the store.

TrapThe cookie is signed with secret_key, not encrypted. Users can read it but can't change it, so never put secrets in it.

Flask-Login and passwords

Turn the session's user ID into a user@login_manager.user_loader
Log a user in and outlogin_user(user)logout_user()
Protect a view@login_required
Hash a password, then check it at logingenerate_password_hash(pw)check_password_hash(hash, pw)

TipFlask-Login doesn't register users or store passwords. Your login view checks the hash.

TrapNever use a fast hash such as SHA-256. The default is salted scrypt, and String(128) is too short for its result.

API tokens and roles

Verify a JSON Web Token (JWT)jwt.decode(t, key, algorithms=['HS256'])A bad token gets 401.
Check the user's role@require_role('admin')Your decorator returns 403.
Revoke a tokenKeep a denylist, because a JWT stays valid until it expires.

TrapPass a fixed algorithms list to jwt.decode(), never one read from the token.

SQLAlchemy & migrations

models, queries, transactions and pools

Set up Flask-SQLAlchemy

Set the URI, bind db, then map a tableSQLALCHEMY_DATABASE_URIdb.init_app(app)class User(db.Model):
Create missing tablesdb.create_all()It needs an app context.

Trapdb.create_all() never alters existing tables. Schema changes go through migrations.

Query the database

Load a row by primary keydb.session.get(User, 1)It replaced Query.get().
Query the old wayUser.query.filter_by(name='j').first()
Query the 2.0 wayq = db.select(User).filter_by(name='j')db.session.execute(q).scalar_one()
Get one page of resultsUser.query.paginate(page=1, per_page=20)

Tipfilter_by() takes keyword equality checks, and filter() takes full expressions.

Relate models

Link a child table to its parentdb.ForeignKey('user.id')db.relationship('Post')
Add the reverse side, the old waybackref='author'
Add the reverse side, the newer wayback_populates='author'
Join many to many, or one to onesecondary=tagsuselist=False

TrapRelationships load lazily, so a loop over posts runs a query for each one. This is the N+1 query problem, and selectinload() avoids it.

Migrations with Flask-Migrate

Create the folder once, then write a scriptflask db initflask db migrate -m "msg"
Apply pending scripts, or revert the lastflask db upgradeflask db downgrade

TrapAlembic, which Flask-Migrate runs, can't detect renames. A rename shows up as a dropped column and a new one, so review each script.

Transactions and connection pools

Save the changes together, or undo themdb.session.commit()db.session.rollback()
Lock a row until the commitwith_for_update=TrueLock rows in a fixed order to stop deadlocks.
Size the pool'pool_size': 10'max_overflow': 20Set these in SQLALCHEMY_ENGINE_OPTIONS.
Replace old and dead connections'pool_recycle': 3600'pool_pre_ping': True

TrapThe pool is per process. Four Gunicorn workers at 10 plus 20 can open 120 connections, so stay under max_connections.

App structure & config

factories, blueprints and environments

Application factory and blueprints

Build the app in a functiondef create_app(cfg):db.init_app(app)app.register_blueprint(bp)
Say why it helpsEach test builds an app with its own config.
Avoid circular importsCreate db in its own module. Reach the app through current_app.
Lay out a large projectAn app/ package with a folder per blueprint.
Group routes under a pathBlueprint('auth', __name__, url_prefix='/auth')

TipA blueprint isn't an app. It records operations and replays them at registration.

Config and environment variables

Load the environment's config class, or FLASK_ variablesapp.config.from_object(cfg)app.config.from_prefixed_env()
Set what the flask command readsFLASK_APP=helloFLASK_DEBUG=1
Load .env under another serverload_dotenv()
Spot the variable Flask 2.3 removedFLASK_ENV=development

TrapA 'dev-secret-key' fallback in the base class also reaches production. Check for SECRET_KEY at startup.

REST APIs, errors & tests

JSON endpoints, versions, logs and test clients

REST, versions and serialization

Return the right statusreturn jsonify(obj), 201return '', 204
Validate the bodyUse marshmallow, and return 400 with the field errors.
Version with one blueprint per versionurl_prefix='/api/v1'
Version by headerAccept: application/vnd.app.v2+json
Turn a model into plain typesuser.to_dict()UserSchema().dump(user)
Teach every response a new typeSubclass DefaultJSONProvider and assign it to app.json.

Trapjsonify() on a model object raises TypeError. The default provider also writes dates in HTTP format.

Errors, exceptions and logs

Handle a status code, and set it yourself@app.errorhandler(404)return page, 404
Raise your own error typeraise APIError('No user', 404)@app.errorhandler(APIError)
Stop logs from filling the diskRotatingFileHandler('logs/app.log')Set maxBytes and backupCount.
Configure logging before you create the applogging.config.dictConfig(cfg)

TrapA handler for base Exception also catches errors such as 405, so a client mistake becomes a 500 page.

Test client and fixtures

Build a test client in a pytest fixtureapp = create_app('testing')app.test_client()
Send and read JSONclient.post('/api', json=data)response.get_json()
Test a function that reads requestwith app.test_request_context():
Cover more than successCheck 400, 401, 403, 404 and the shape of the JSON.

TipTESTING = True makes exceptions reach the test instead of becoming 500 pages.

Mock dependencies

Replace a function for one test, then check the call@patch('app.routes.send_email')mock.assert_called_once()
Set what it returns, or make it raisemock.return_value = Truemock.side_effect = TimeoutError

TrapPatch where the name is looked up, not where it is defined.

Security & deployment

fix common vulnerabilities and run in production

Common vulnerabilities

Stop SQL injectionUse the ORM or bound parameters.
Stop cross-site scripting (XSS)Never use |safe or Markup() on user data.
Stop insecure direct object referencesCheck that the current user owns each record.
Force HTTPS and security headersTalisman(app, force_https=True)

TrapDebug mode in production lets the Werkzeug debugger run Python from the browser. Only a PIN guards it.

Rate limiting

Create the limiterLimiter(get_remote_address, app=app)
Limit one route@limiter.limit("5 per minute")Over the limit, a client gets 429.

TrapThe default in-memory storage is only for development, because each worker keeps its own counts. Set storage_uri to Redis.

Deploy to production

Run a WSGI server with four workersgunicorn -w 4 app:appStart at about two workers per CPU core. Use Waitress on Windows.
Put a reverse proxy in frontnginx handles HTTPS and static files.

TrapBehind a proxy, wrap the app in ProxyFix, or Flask sees the proxy's address and scheme, not the client's.

Caching, tasks & WebSockets

faster responses, and work outside a request

Flask-Caching

Cache a view for five minutes@cache.cached(timeout=300)
Cache a function per set of arguments@cache.memoize(timeout=600)
Drop one entry, or empty the cachecache.delete_memoized(fn, id)cache.clear()

TrapNever cache a per-user view under a shared key, or one user may see another user's page.

Celery tasks and async views

Queue a job and return at oncesend_email_task.delay(recipient, subject, body)A broker carries the message to a worker.
Give tasks an app contextWrap each call in app.app_context().
Pass data to a task, and survive retriesPass IDs, not model objects. Make tasks idempotent, so running one twice has the same effect.
Write an async view (2.0+)async def dashboard():The request still holds one worker. A mostly async app fits Quart better.

TrapA thread started in a view lives only in process memory, and Flask cancels asyncio.create_task tasks when the view returns.

WebSockets with Flask-SocketIO

Handle an event, then send to a room or everyone@socketio.on('join')emit('m', d, room=room)emit('m', d, broadcast=True)
Share events across processesSocketIO(app, message_queue=url)

TrapGunicorn can run only one worker process for Socket.IO. Scale with several processes and sticky sessions.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 60 questions it links, not just the 9 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.